/* Epilogue web app: "The Re-shelved Shelf".
   Tokens and components mirror DESIGN.md exactly (colours, spacing, radius,
   type). The header, You chip, bottom action bar, door cards and sheet are
   this app's own extensions of that world (DESIGN.md was written for the
   Streamlit page); they reuse the same tokens rather than inventing new
   ones. See .impeccable/surfaces/static-index-html.md for the brief. */

/* Self-hosted so the installed app has the face offline (sw.js caches
   both files). Both weights point at the same downloaded file: fetching
   https://fonts.googleapis.com/css2?family=Cabin:wght@400;700 with a
   browser User-Agent (2026-09-20) shows Google serving Cabin's latin
   range as one variable font for both weights, not two static files -
   the same fact config.toml's own comment already recorded. */
@font-face {
  font-family: "Cabin";
  src: url("/static/fonts/cabin-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Cabin";
  src: url("/static/fonts/cabin-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Colour (DESIGN.md "Colors") */
  --ink: #111111;
  --paper: #ffffff;
  --paper-grey: #f2f2f2;
  --share-orange: #e2562b;
  /* Darkened from #e8944a/#c9a227 (2.40:1 / 2.42:1 on white, under the
     1.4.11 non-text 3:1 minimum) by the smallest proportional amount that
     clears 3:1 with a small safety margin - see static/js/shelf.js's
     RAMP_FIX comment for the full ratio history. */
  --share-amber: #cc8241;
  --share-mustard: #b28f23;
  --share-teal: #4f8a8b;
  --share-navy: #2f4b7c;
  --link-blue: #1d4ed8;
  /* A pale tint of share-orange for the segmented control's/pills' selected
     state ("orange text on a pale orange tint", DESIGN.md's Components
     section). DESIGN.md names the effect but not a hex; derived here as
     share-orange at 15% over paper. */
  --orange-tint: #fbe6df;
  /* Selected-spine lift shadow (finish review fix 5): themed per ground so
     the lift stays visible - Night Library overrides this below. */
  --lift-shadow: rgba(17, 17, 17, 0.18);

  /* Spacing (DESIGN.md "spacing") */
  --space-hairline: 2px;
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 12px;
  --space-lg: 24px;
  --space-row: 40px;
  /* PLAN 7f item 2's wider spines. Mirrored as SPINE_CELL_WIDTH in
     static/js/shelf.js (no build step joins the two, so both must move
     together by hand); 3 cells still fit one row at 390px with the
     app's own gutters and the 8px column gap. */
  --spine-cell-width: 100px;

  /* Shape (DESIGN.md "rounded", "Shapes") */
  --radius: 2px;
  --border: 1px solid var(--ink);
  --border-dashed: 1px dashed var(--ink);

  /* Type (DESIGN.md "typography") */
  --font-body: "Cabin", sans-serif;
  /* A theme's own display face reads this name (THEME_MECHANISM.md's
     per-theme font stylesheet rule); Paper has none of its own, so this
     stays Cabin and nothing on Paper changes (THEME_NIGHT_LIBRARY.md
     item 1). */
  --font-display: "Cabin", sans-serif;
  --text-share: 0.85rem;
  /* Raised from 11px (UX_REVIEW.md section 3 item 3: the honesty flags
     were the smallest type on the shelf); the words are also spelled out
     in full now (static/js/shelf.js's stickersFor), so the extra size
     goes toward that longer text staying legible, not just bigger letters. */
  --text-sticker-foot: 13px;
  --text-sticker-detail: 12px;

  /* DESIGN.md's own sizes are named from Streamlit widgets ("st.title
     size"), not literal values, and cover a single page; this app adds
     pages, a header and section headings DESIGN.md never sized, so this
     is its own small scale rather than scattered one-off literals. */
  --text-body: 1rem;
  --text-heading-sm: 1.125rem;
  --text-heading-md: 1.375rem;
  --text-heading-lg: 1.5rem;
  --text-display: 4.5rem;

  /* App shell (this app's own extension of DESIGN.md's world) */
  --header-height: 56px;
  --content-width: 560px;
  --motion-fast: 200ms;

  /* Accent vs ramp split (PLAN 7h item 1, theme mechanism): --accent,
     --accent-tint and --link are the names selected pills, toggles,
     ::selection, the focus ring and links actually read from here on.
     Literal hexes, not var(--share-orange) etc - a custom property
     substitutes var() at computed-value time on the element it is
     declared on, so pointing at --share-orange here would let any block
     that redeclares --share-orange on :root (Easier to read's deeper
     ramp) repaint the accent too, which is exactly what this split exists
     to prevent (review finding 2). --share-orange, --orange-tint and
     --link-blue stay defined too (the five-step ramp still needs
     share-orange as its own step); nothing changes on Paper, where these
     literals just match the old values. */
  --accent: #e2562b;
  --accent-tint: #fbe6df;
  --link: #1d4ed8;

  /* Sticker colours as tokens (.sticker reads only these, never --paper/
     --ink directly), so a theme can recolour the sticker on its own. */
  --sticker-ground: var(--paper);
  --sticker-ink: var(--ink);

  /* Ramp label text per step (DESIGN.md's Measured Contrast Rule, now a
     token instead of the JS lookup tables it used to live in): ink for
     the first four steps, paper for navy - a theme overriding a step's
     fill can carry its label colour along with it via the matching
     data-step rule below. */
  --share-orange-text: var(--ink);
  --share-amber-text: var(--ink);
  --share-mustard-text: var(--ink);
  --share-teal-text: var(--ink);
  --share-navy-text: var(--paper);

  /* Touch target minimum (WCAG 2.5.8): every 44px hit-area literal below
     reads this token, so Easier to read can raise it once, here. */
  --target-min: 44px;
}

* {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, p, dl, dd {
  margin: 0;
}

img {
  max-width: 100%;
  display: block;
}

a {
  color: var(--link);
}

button, input, textarea {
  font: inherit;
  color: inherit;
}

::selection {
  background: var(--accent);
  color: var(--paper);
}

:focus-visible {
  outline: 2px solid var(--link);
  outline-offset: 2px;
}

.tabular-nums {
  font-variant-numeric: tabular-nums;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ------------------------------------------------------------- skip link */

/* First focusable element in the document (2.4.1 Bypass Blocks). Off-screen
   until focused, then a normal 44px button-shaped target in the same paper/
   ink/2px-radius world as everything else - no new colour or shape. */
.skip-link {
  position: fixed;
  top: var(--space-sm);
  left: var(--space-sm);
  z-index: 100;
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  padding: 0 var(--space-md);
  background: var(--paper);
  color: var(--ink);
  border: var(--border);
  border-radius: var(--radius);
  text-decoration: underline;
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: translateY(0);
}

/* ---------------------------------------------------------------- header */

.app-header {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  padding-top: max(var(--space-md), env(safe-area-inset-top));
  background: var(--paper);
  border-bottom: var(--border);
}

.app-name {
  font-size: var(--text-heading-md);
  font-weight: 700;
  line-height: 1.2;
}

.chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--target-min);
  max-width: 55vw;
  padding: var(--space-sm) var(--space-md);
  background: var(--paper);
  color: var(--ink);
  border: var(--border);
  border-radius: var(--radius);
  font-size: var(--text-share);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}

.chip:hover {
  background: var(--paper-grey);
}

/* ------------------------------------------------------------ app shell */

.app-main {
  max-width: var(--content-width);
  margin-inline: auto;
  padding: var(--space-lg) var(--space-lg) var(--space-lg);
}

.page-lede {
  margin-bottom: var(--space-lg);
  font-size: var(--text-body);
}

.page-title {
  font-size: var(--text-heading-lg);
  font-weight: 700;
  line-height: 1.25;
  margin-bottom: var(--space-sm);
}

.action-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  background: var(--paper);
  border-top: var(--border);
  padding: var(--space-md) var(--space-lg);
  padding-bottom: max(var(--space-md), env(safe-area-inset-bottom));
}

.action-bar[hidden] {
  display: none;
}

.action-bar-inner {
  max-width: var(--content-width);
  margin-inline: auto;
  display: flex;
  gap: var(--space-sm);
}

/* ------------------------------------------------------------- buttons */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-height: var(--target-min);
  padding: 0 var(--space-md);
  background: var(--paper);
  color: var(--ink);
  border: var(--border);
  border-radius: var(--radius);
  font-size: var(--text-body);
  font-weight: 400;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
}

.btn:hover {
  background: var(--paper-grey);
}

.btn:active {
  transform: translateY(1px);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

.btn-emphasis {
  font-weight: 700;
}

.btn-block {
  width: 100%;
}

.btn-text {
  display: inline-flex;
  min-height: var(--target-min);
  align-items: center;
  background: none;
  border: none;
  color: var(--ink);
  text-decoration: underline;
  cursor: pointer;
  padding: 0;
  font-size: var(--text-body);
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}

.barcode-video {
  width: 100%;
  margin-bottom: var(--space-md);
  background: var(--ink);
  border: var(--border);
  border-radius: var(--radius);
}

.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: var(--target-min);
  color: var(--ink);
  text-decoration: none;
  margin-bottom: var(--space-md);
}

.back-link:hover {
  text-decoration: underline;
}

/* ----------------------------------------------------------- door cards */

.door-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
}

.door-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  min-height: 96px;
  padding: var(--space-lg) var(--space-md);
  background: var(--paper);
  border: var(--border);
  border-radius: var(--radius);
  text-align: left;
  cursor: pointer;
}

.door-card:hover {
  background: var(--paper-grey);
}

.door-card-icon {
  flex: none;
  width: 32px;
  height: 32px;
}

.door-card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.door-card-title {
  font-size: var(--text-heading-sm);
  font-weight: 700;
}

.door-card-desc {
  font-size: var(--text-share);
}

/* --------------------------------------------------------- shelves list */

.shelves-heading {
  font-weight: 700;
  margin-bottom: var(--space-sm);
}

.shelves-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.shelf-item {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  padding: var(--space-md) 0;
  background: none;
  border: none;
  border-bottom: 1px solid var(--ink);
  text-align: left;
  cursor: pointer;
}

.shelf-item:hover {
  background: var(--paper-grey);
}

.shelf-thumb {
  flex: none;
  width: 44px;
  height: 44px;
  border: var(--border);
  border-radius: var(--radius);
  background: var(--paper-grey);
  object-fit: cover;
}

.shelf-item-name {
  font-weight: 700;
}

.shelf-item-count {
  font-size: var(--text-share);
}

.empty-state {
  padding-top: var(--space-md);
  border-top: 3px solid var(--ink);
}

.empty-state p {
  margin-top: var(--space-sm);
  font-size: var(--text-share);
}

/* -------------------------------------------------------------- fields */

.field {
  margin-bottom: var(--space-lg);
}

.field-label {
  display: block;
  font-weight: 700;
  margin-bottom: var(--space-sm);
}

.field-input {
  display: block;
  width: 100%;
  min-height: var(--target-min);
  padding: 0 var(--space-md);
  background: var(--paper);
  color: var(--ink);
  border: var(--border);
  border-radius: var(--radius);
}

.field-input:focus-visible {
  border-color: var(--link);
}

.field-hint {
  margin-top: var(--space-xs);
  font-size: var(--text-share);
}

.field-error {
  margin-top: var(--space-sm);
  color: var(--ink);
  font-weight: 700;
}

/* ------------------------------------------------ books at home (A2) */

/* The set-up page's door (HOME_SPEC.md): a link in the export drop's own
   shape, so the two ways in without the questions read as a pair. */
.home-door {
  flex-direction: column;
  gap: var(--space-xs);
  margin-top: var(--space-md);
  margin-bottom: var(--space-lg);
  color: var(--ink);
  text-decoration: none;
}

.home-door:hover {
  text-decoration: underline;
}

.home-door:focus-visible {
  outline: 2px solid var(--link);
  outline-offset: 2px;
}

.home-door-hint {
  font-size: var(--text-share);
  font-weight: 400;
}

.mark-notes {
  margin-bottom: var(--space-md);
}

.mark-list {
  list-style: none;
  margin: 0 0 var(--space-lg);
  padding: 0;
  border-top: var(--border);
}

/* Phone width: the three choices run the full row under the cover, so
   "Not started" does not wrap on its own under Easier to read. From
   600px they sit under the title instead. */
.mark-item {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr);
  grid-template-areas:
    "cover text"
    "choices choices";
  column-gap: var(--space-md);
  row-gap: var(--space-sm);
  padding: var(--space-md) 0;
  border-bottom: var(--border);
}

@media (min-width: 600px) {
  .mark-item {
    grid-template-areas:
      "cover text"
      "cover choices";
  }
}

.mark-item > .mark-cover {
  grid-area: cover;
}

.mark-item > .mark-text {
  grid-area: text;
}

/* Two classes, so this beats .pill-group's own margin, which comes later
   in this file. */
.mark-item > .mark-choices {
  grid-area: choices;
  margin: 0;
}

.mark-cover {
  display: block;
  width: 40px;
  height: 60px;
  object-fit: cover;
  background: var(--paper-grey);
  border: var(--border);
}

.mark-title {
  margin: 0;
  font-weight: 700;
  overflow-wrap: anywhere;
}

.mark-author {
  margin: 0;
  font-size: var(--text-share);
}

.action-bar-inner:has(.action-bar-note) {
  flex-wrap: wrap;
}

.action-bar-note {
  flex-basis: 100%;
  margin: 0;
  font-size: var(--text-share);
  font-weight: 700;
}

.file-drop {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 96px;
  padding: var(--space-lg) var(--space-md);
  background: var(--paper-grey);
  border: var(--border);
  border-radius: var(--radius);
  text-align: center;
  cursor: pointer;
  font-weight: 700;
}

/* Invisible but sized to the whole 96px label, not clipped to 1x1px: the
   label is the real target (2.5.8), and clip-hiding also clipped the
   input's own focus outline (2.4.7). */
.file-drop input[type="file"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.file-drop:has(input:focus-visible) {
  outline: 2px solid var(--link);
  outline-offset: 2px;
}

/* Barcode-photo control (ISBN page, item 6): a .btn label with its real
   file input positioned over it at zero opacity, the same technique as
   file-drop above and for the same reason - the input, not a
   script-triggered click(), is what needs to be the thing a phone
   browser opens the camera for, and what keyboard focus lands on.
   The You page's "Update with a newer export" button (.file-button,
   REFRESH_SPEC.md) is the same control. */
.barcode-photo-label,
.file-button {
  position: relative;
  overflow: hidden;
}

.barcode-photo-label input[type="file"],
.file-button input[type="file"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.barcode-photo-label:has(input:focus-visible),
.file-button:has(input:focus-visible) {
  outline: 2px solid var(--link);
  outline-offset: 2px;
}

.checkbox-field {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-height: var(--target-min);
  margin-bottom: var(--space-lg);
  cursor: pointer;
}

.checkbox-field input {
  width: 20px;
  height: 20px;
  flex: none;
  accent-color: var(--ink);
}

/* --------------------------------------------------------------- pills */

.pill-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}

/* Skeleton pills (UX_REVIEW.md section 3 item 10): shown in a pill-group
   while its options are still loading, so the set-up page never shows a
   blank question. Same footprint as a real pill so nothing reflows when
   the real pills (or the plain "could not load" note) replace them. */
.skeleton-pill {
  display: inline-block;
  min-height: var(--target-min);
  width: 72px;
  background: var(--paper-grey);
  border: var(--border);
  border-radius: var(--radius);
}

@media (prefers-reduced-motion: no-preference) {
  .skeleton-pill {
    animation: skeleton-pulse 1.2s ease-in-out infinite;
  }
}

@keyframes skeleton-pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

.pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--target-min);
  padding: 0 var(--space-md);
  background: var(--paper);
  color: var(--ink);
  border: var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}

.pill:hover {
  background: var(--paper-grey);
}

.pill.is-selected {
  /* Ink text, not the accent (3.12:1 on the tint, under 4.5:1): the accent
     stays as the tint and the border, which still carries the "chosen"
     meaning without darkening it (share-orange is also a spine colour).
     Ink on the tint measures 15.72:1. */
  background: var(--accent-tint);
  color: var(--ink);
  border-color: var(--accent);
}

/* Night Library only (finish review fix 4): the approved comp shows gold
   text on the tint, and accent-on-tint measures 6.12:1 here (Paper's own
   3.12:1 is why the shared rule above keeps ink) - see
   review/contrast/night-library.md. */
[data-theme="night-library"] .pill.is-selected,
[data-theme="night-library"] .toggle-option.is-selected {
  color: var(--accent);
}

/* ------------------------------------------------------- segmented toggle */

/* Two segmented toggles side by side (Everyone/For you, Spines/Covers -
   PLAN 7f item 2): the same 8px row gap used between pills and buttons,
   each toggle sharing the row's width equally (its own two options then
   split that half again, via .toggle-option's existing flex: 1). */
.toggle-row {
  display: flex;
  gap: var(--space-sm);
}

.toggle-row .toggle {
  flex: 1;
}

.toggle {
  display: flex;
  border: var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: var(--space-md);
}

.toggle-option {
  flex: 1;
  min-height: var(--target-min);
  padding: 0 var(--space-sm);
  background: var(--paper);
  color: var(--ink);
  border: none;
  border-right: 1px solid var(--ink);
  cursor: pointer;
  font-size: var(--text-body);
}

.toggle-option:last-child {
  border-right: none;
}

.toggle-option.is-selected {
  /* Same fix as .pill.is-selected: ink text on the tint, the accent kept
     as a ring rather than a border (box-shadow so it doesn't disturb the
     toggle's own layout or its ink separators between options). */
  background: var(--accent-tint);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--accent);
  font-weight: 700;
}

.toggle-option:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

.empty-state a {
  /* Was --text-share (13.6px) in a 97.5x16px box, under both the 24px AA
     minimum and the app's own 44px target: body size, normal line height,
     44px tall, same shape as the app's other inline text links. Scoped to
     .empty-state (design finish review item 2) rather than a class of its
     own, so the For you empty state's paragraph carries no styling beyond
     .empty-state p and sits exactly like the home page's own empty state. */
  display: inline-flex;
  align-items: center;
  min-height: var(--target-min);
  font-size: var(--text-body);
  line-height: 1.4;
  color: var(--link);
}

/* ---------------------------------------------------------------- sheet */

.sheet {
  padding: var(--space-md);
  margin-bottom: var(--space-lg);
  background: var(--paper);
  border: var(--border);
  border-radius: var(--radius);
}

.sheet-message {
  margin-bottom: var(--space-md);
}

.sheet-actions {
  display: flex;
  gap: var(--space-sm);
}

.sheet-code {
  font-size: var(--text-heading-lg);
  font-weight: 700;
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--space-xs);
}

/* ---------------------------------------------------------- shelf view */

/* Rename control (PLAN 7f item 2): the 44px text button sits next to the
   name; editing swaps both for a flexible input plus Save/Cancel. */
.shelf-title-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}

.shelf-title-row .page-title {
  margin-bottom: 0;
}

.shelf-title-row .field-input {
  width: auto;
  flex: 1 1 120px;
  min-width: 0;
}

/* The headline above the shelf (PLAN 7f item 2): "Most often finished: A,
   B and C" (Everyone) or "You'd most likely finish: A, B and C" (For
   you) - same bold-body treatment as .compare-caption below it. */
.shelf-headline {
  margin-bottom: var(--space-md);
  font-weight: 700;
}

.counts-line {
  margin-bottom: var(--space-lg);
  font-size: var(--text-share);
  line-height: 1.4;
  white-space: normal;
  overflow-wrap: break-word;
}

.shelf-section {
  margin-bottom: var(--space-lg);
}

.shelf-heading {
  font-weight: 700;
  margin-bottom: var(--space-sm);
}

.shelf-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  column-gap: var(--space-sm);
  row-gap: var(--space-row);
}

.shelf-cell {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: var(--spine-cell-width);
  flex: none;
}

.spine {
  position: relative;
  min-width: var(--spine-cell-width);
  min-height: 120px;
  padding: var(--space-sm) var(--space-xs) var(--space-lg);
  background: var(--spine-fill, var(--share-navy));
  color: var(--spine-text, var(--paper));
  border: var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  opacity: 1;
  transform: translateY(0);
  /* A long title is truncated at 26 characters (spineLabel) before it
     ever reaches here; this is the second guard, since 26 characters set
     vertically can still be taller than a short spine. Without it the
     label runs past the spine's own bottom edge, over the shelf line,
     the percent and the foot sticker. */
  overflow: hidden;
}

/* Painted by step, not by inline hex (PLAN 7h item 1): pages/shelf.js sets
   data-step from the row's own colour (theme.js's rampStepFor) and never
   an inline colour. A theme overriding a step's fill and label overrides
   these two custom properties, or the --share-<step> and
   --share-<step>-text tokens
   they read from, either works. No rule needed for "no step" (unsure or
   unidentified spines): .spine's own background/color already default to
   share-navy/paper via the var()s below, same as before this change. */
.spine[data-step="orange"] { --spine-fill: var(--share-orange); --spine-text: var(--share-orange-text); }
.spine[data-step="amber"] { --spine-fill: var(--share-amber); --spine-text: var(--share-amber-text); }
.spine[data-step="mustard"] { --spine-fill: var(--share-mustard); --spine-text: var(--share-mustard-text); }
.spine[data-step="teal"] { --spine-fill: var(--share-teal); --spine-text: var(--share-teal-text); }
.spine[data-step="navy"] { --spine-fill: var(--share-navy); --spine-text: var(--share-navy-text); }

/* Same pattern for the book page's cover frame and its fallback block
   (pages/book.js sets data-step on whichever one is showing);
   --book-frame/--book-frame-text default to share-navy/paper below, same
   as a spine with no step. The frame carries the border colour, the
   fallback block inherits it for its own background (finish review
   fix 1: the frame no longer paints an inline hex). */
.book-cover-frame[data-step="orange"], .book-cover-fallback[data-step="orange"] { --book-frame: var(--share-orange); --book-frame-text: var(--share-orange-text); }
.book-cover-frame[data-step="amber"], .book-cover-fallback[data-step="amber"] { --book-frame: var(--share-amber); --book-frame-text: var(--share-amber-text); }
.book-cover-frame[data-step="mustard"], .book-cover-fallback[data-step="mustard"] { --book-frame: var(--share-mustard); --book-frame-text: var(--share-mustard-text); }
.book-cover-frame[data-step="teal"], .book-cover-fallback[data-step="teal"] { --book-frame: var(--share-teal); --book-frame-text: var(--share-teal-text); }
.book-cover-frame[data-step="navy"], .book-cover-fallback[data-step="navy"] { --book-frame: var(--share-navy); --book-frame-text: var(--share-navy-text); }

.spine.is-outline {
  background: var(--paper);
  color: var(--ink);
}

.spine.is-unsure {
  border: var(--border-dashed);
}

.spine:disabled {
  cursor: default;
}

.spine.is-selected {
  border: 3px solid var(--ink);
  outline: 3px solid var(--ink);
  outline-offset: 2px;
  transform: translateY(-6px);
  box-shadow: 0 6px 12px var(--lift-shadow);
  animation: none;
}

.spine-label {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
  /* Raised from --text-share (0.85rem/400) so the vertical title reads at
     arm's length on a phone (PLAN 7f item 2) - the wider cell (see
     --spine-cell-width) gives this bigger, bolder glyph room to sit in. */
  font-size: var(--text-body);
  font-weight: 700;
  /* max-height, not height: a short title keeps its own natural size.
     100% resolves against .spine's content box, which its own padding
     (8px top, 24px bottom) already narrows to clear of the shelf line,
     the percent and the foot sticker below - text-overflow needs both
     nowrap (already set) and this size limit to ellipsize instead of
     just running past it. */
  max-height: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Covers mode (PLAN 7f item 2): the labelled spine above is always the
   default state, at the same cell width and border/radius; row.cover_url
   only ever adds this image and foot band on top of it, invisible until
   the image actually loads (renderSpineCell's "load" listener adds
   has-cover on .shelf-cell). That way a slow or failed cover leaves the
   labelled spine showing rather than a blank box. The band carries the
   share colour, never a border (Don't use a border colour other than
   ink), so the five-step ramp still reads across the shelf. */
.spine-cover-img {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: var(--space-md);
  width: 100%;
  height: auto;
  /* contain, not cover: the whole cover stands on the band with paper
     above it, rather than a crop filling the cell. */
  object-fit: contain;
  object-position: bottom center;
  opacity: 0;
}

.spine-cover-band {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--space-md);
  background: var(--spine-fill, var(--share-navy));
}

.shelf-cell.has-cover .spine {
  background: var(--paper);
}

.shelf-cell.has-cover .spine-label {
  display: none;
}

.shelf-cell.has-cover .spine-cover-img {
  opacity: 1;
}

.shelf-bar {
  height: 4px;
  min-height: 4px;
  background: var(--ink);
  margin-inline: calc(var(--space-sm) / -2);
}

.spine-percent {
  margin-top: var(--space-xs);
  text-align: center;
  font-size: var(--text-share);
}

/* The ramp step's share word ("58% · high"): always in the spine's
   accessible name (shelf.js's spineName), but only shown on screen in
   Easier to read, where :root[data-read="easier"] below switches it on. */
.spine-share-word {
  display: none;
}

.spine.is-selected + .shelf-bar + .spine-percent,
.shelf-cell:has(.spine.is-selected) .spine-percent {
  font-weight: 700;
}

.stickers {
  position: absolute;
  left: 3px;
  right: 3px;
  bottom: 3px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px;
}

.sticker {
  display: inline-block;
  padding: 2px 3px;
  background: var(--sticker-ground);
  color: var(--sticker-ink);
  border: 1px solid var(--sticker-ink);
  border-radius: var(--radius);
  font-size: var(--text-sticker-foot);
  line-height: 1.2;
  white-space: nowrap;
}

.median-divider {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 1em;
  margin: 10px 0;
  border-top: var(--border-dashed);
}

.median-divider-label {
  position: relative;
  top: -0.7em;
  padding: 0 10px;
  background: var(--paper);
  font-size: var(--text-share);
}

/* ----------------------------------------------------------- compare view */

.compare-caption {
  margin-bottom: var(--space-md);
  font-weight: 700;
}

.compare-blend-note {
  margin-top: var(--space-md);
  font-size: var(--text-share);
}

/* Sticker key line (UX_REVIEW.md section 3 item 3): a plain-words legend
   for the shelf's foot stickers, sitting under the shelf on every view. */
.shelf-key-line {
  margin-top: var(--space-md);
  font-size: var(--text-share);
}

/* ------------------------------------------------------------- book page */

.book-cover-frame {
  width: 160px;
  margin-bottom: var(--space-lg);
  padding: var(--space-xs);
  border: 4px solid var(--book-frame, var(--share-navy));
  border-radius: var(--radius);
  background: var(--paper);
}

.book-cover-img {
  width: 100%;
  height: auto;
}

.book-cover-fallback {
  width: 100%;
  aspect-ratio: 2 / 3;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  background: var(--book-frame, var(--share-navy));
  color: var(--book-frame-text, var(--paper));
  text-align: center;
  font-weight: 700;
}

.book-title {
  font-size: var(--text-heading-lg);
  font-weight: 700;
  margin-bottom: var(--space-xs);
}

.book-author {
  font-size: var(--text-share);
  margin-bottom: var(--space-lg);
}

.book-percent-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
  flex-wrap: wrap;
}

.book-percent {
  font-size: var(--text-display);
  font-weight: 700;
  line-height: 1;
}

/* The share word inherits .book-percent's display size and weight by
   default, which reads at 4.5rem, bold, and wraps to a second line at
   390px; set back to body size and normal weight so it reads as a word
   beside the number, not a second number (finish review fix 3). */
.book-percent .spine-share-word {
  font-size: var(--text-body);
  font-weight: 400;
}

.book-percent-aside {
  font-size: var(--text-share);
}

.book-sentence {
  margin-bottom: var(--space-md);
}

.book-match-note {
  margin-bottom: var(--space-md);
  font-weight: 700;
}

.detail-stickers {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: var(--space-sm) 0 var(--space-lg);
}

.detail-stickers .sticker {
  font-size: var(--text-sticker-detail);
  padding: 3px 6px;
}

/* --------------------------------------------- "What's it about?" summary
   SUMMARY_SPEC.md section 3 items 5 and 6: a disclosure toggle in the same
   pattern as the "Not this book" picker below, one self-contained slot
   between the stickers and the "Photo said"/"Matched" dl. The panel reuses
   .sheet, same as .picker does - an in-flow panel, never an overlay, 12px
   above it, the same margin-top .picker uses. */

.summary-panel {
  margin-top: var(--space-md);
}

/* The dl below (.book-lines) has no top margin, so the slot carries the
   gap under the toggle and panel: the same --space-lg the stickers leave
   above it. An empty slot (a book with no work key) adds nothing. */
#summary-slot:not(:empty) {
  margin-bottom: var(--space-lg);
}

/* "How we estimate this" (PLAN 7h owner steer (4)) sits just above this
   summary slot, the two toggles together under the stickers (DESIGN.md's
   Book page order, finish review fix 1); same rule, so either toggle
   carries its own gap under it regardless of order. Skipped for a row
   with no estimate (finish review fix 4), where the slot stays empty. */
#how-we-estimate-slot:not(:empty) {
  margin-bottom: var(--space-lg);
}

/* Sparse caption (PLAN 7h owner steer (2)): sits nearer the number it
   explains than the sentence under it (finish review, second pass). */
.book-percent-row:has(+ #sparse-note:not(:empty)) {
  margin-bottom: var(--space-xs);
}

#sparse-note:not(:empty) {
  margin-bottom: var(--space-md);
}

/* The one-line "why" under the estimate sentence (WHY_SPEC.md): aside
   size, like the sparse caption, so it reads as a note on the sentence
   above rather than a second headline. --text-share is a rem value, so
   Easier to read's root font-size bump scales it with no override. */
.book-why {
  max-width: 70ch;
  font-size: var(--text-share);
  margin-top: calc(-1 * var(--space-sm));
  margin-bottom: var(--space-md);
}

/* Its detail, inside the "How we estimate this" panel after the general
   note: set apart by the theme's own solid rule (not the dashed one,
   which DESIGN.md keeps for doubt) and a bold label, so the panel reads
   as two parts rather than one block (impeccable finding 2). */
.why-detail {
  margin-top: var(--space-md);
  padding-top: var(--space-md);
  border-top: var(--border);
}

.why-detail-label {
  font-weight: 700;
  margin-bottom: var(--space-sm);
}

.why-detail-lines {
  max-width: 70ch;
  margin: var(--space-sm) 0;
  padding-left: 1.25em;
}

.why-detail-lines li + li {
  margin-top: var(--space-xs);
}

.summary-line {
  max-width: 70ch;
}

.summary-line + .summary-line {
  margin-top: var(--space-sm);
}

/* The two plain Gemini attribution notes (the summary's own label, and
   the themes pair's "not verified" line) stay small print, out of the
   way but there for a reader who looks: the stickers' detail size on
   the normal scale, the share size under Easier to read so it never
   drops below it there. Design finish review, 2026-09-23: the
   "Possible themes" and reader's-own-StoryGraph lines no longer use
   this class - see .summary-line-labelled below - so only the two
   Gemini notes are small now. */
.summary-line.summary-line-share {
  font-size: var(--text-sticker-detail);
  margin-top: var(--space-md);
}

:root[data-read="easier"] .summary-line.summary-line-share {
  font-size: var(--text-share);
}

/* The "Possible themes" line and the reader's own StoryGraph line
   (design finish review, 2026-09-23): --text-share, not the small
   print above, each with a bold lead term in the style of the book
   page's own "Photo said:" (addLine's <dt>). --text-share is already a
   rem value, so it scales correctly under Easier to read's root
   font-size bump with no extra override needed here, the same as body
   copy's own 1rem does. */
.summary-line-labelled {
  max-width: 70ch;
  font-size: var(--text-share);
  margin-top: var(--space-md);
}

.summary-line-label {
  font-weight: 700;
}

/* Sets the "Possible themes" pair - this line and the "Written by
   Gemini... not verified" note that follows it - apart from what
   precedes it: the dashed 1 px rule DESIGN.md otherwise reserves for
   doubt (the unsure-match spine, the median divider), extended here to
   mark Gemini's unverified guess apart from the reader's own StoryGraph
   line above it. */
.summary-line-unverified {
  padding-top: var(--space-sm);
  border-top: var(--border-dashed);
}

/* The attribution note sits close to the themes line it explains - 4 px,
   not the general 12 px share-line gap above - so the pair reads as one
   unit under the dashed rule. Three classes here (0,0,3,0) must stay
   more specific than .summary-line.summary-line-share (0,0,2,0) above,
   or that rule's 12 px silently wins again regardless of source order
   (design finish review, 2026-09-23). */
.summary-line-unverified + .summary-line.summary-line-share {
  margin-top: var(--space-xs);
}

.book-lines {
  margin-bottom: var(--space-lg);
  font-size: var(--text-share);
}

.book-lines dt {
  font-weight: 700;
  display: inline;
}

.book-lines dd {
  display: inline;
  margin: 0;
}

.book-lines .book-line {
  margin-bottom: var(--space-xs);
}

/* -------------------------------------------------- "Not this book" picker
   UX_REVIEW.md #9: a wrong match can be corrected in place, no dialog. The
   panel reuses .sheet (an in-flow panel, never an overlay); only the list
   of candidate rows and their thumbnails are new here. */

.picker {
  margin-top: var(--space-md);
}

.picker-list {
  display: flex;
  flex-direction: column;
}

.picker-option {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  min-height: var(--target-min);
  padding: var(--space-sm) 0;
  background: none;
  border: none;
  border-bottom: 1px solid var(--ink);
  color: var(--ink);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  font-size: var(--text-body);
}

.picker-list .picker-option:last-child {
  border-bottom: none;
}

.picker-option:hover {
  background: var(--paper-grey);
}

.picker-option:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.picker-thumb {
  flex: none;
  width: 32px;
  height: 44px;
  border: var(--border);
  border-radius: var(--radius);
  background: var(--paper-grey);
  object-fit: contain;
}

.picker-option-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.picker-option-title {
  font-weight: 700;
}

.picker-option-author {
  font-size: var(--text-share);
}

.picker-search-link {
  font-weight: 700;
  text-decoration: underline;
}

/* --------------------------------------------------------------- you page */

.you-section {
  margin-bottom: var(--space-lg);
  padding-bottom: var(--space-lg);
  border-bottom: 1px solid var(--paper-grey);
}

.you-section:last-child {
  border-bottom: none;
}

.you-section h2 {
  font-size: var(--text-heading-sm);
  font-weight: 700;
  margin-bottom: var(--space-sm);
}

.how-to {
  padding: var(--space-md);
  background: var(--paper-grey);
  border-radius: var(--radius);
  font-size: var(--text-share);
}

/* Your reading with a profile from an export (REFRESH_SPEC.md section 2):
   the source line as its own paragraph, and the upload block moved in
   under it with room to breathe. */
.profile-summary p + p {
  margin-top: var(--space-sm);
}

.you-update:not(:empty) {
  margin-top: var(--space-md);
}

.you-update .how-to {
  margin-top: var(--space-sm);
}

/* ------------------------------------------------------------------ scan */

.scan-progress {
  margin-top: var(--space-lg);
}

.scan-progress-line {
  margin-bottom: var(--space-md);
  font-weight: 700;
}

.scan-progress-count {
  margin-bottom: var(--space-sm);
}

.scan-cancel-btn {
  margin-bottom: var(--space-md);
}

/* ------------------------------------------------------------------ motion
   One authored entrance (the shelf filling in) and one state change (the
   selected spine coming forward), per DESIGN.md's "Entrance Rule" and
   "One Book Forward Rule". Both cancel under prefers-reduced-motion. */

@media (prefers-reduced-motion: no-preference) {
  .spine {
    animation: spine-enter var(--motion-fast) ease-out backwards;
    animation-delay: calc(var(--spine-index, 0) * 20ms);
    transition: transform var(--motion-fast) ease-out, box-shadow var(--motion-fast) ease-out;
  }

  .spine.is-selected {
    animation: none;
  }
}

@keyframes spine-enter {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  /* axe caught the label mid-fade at 4.24:1 and 3.98:1 (steady state is
     4.80-8.68:1): reaching full opacity by a quarter of the way through
     the rise caps how long the label spends under 4.5:1, while the
     translateY keeps easing for the full 200ms so the rise itself is
     unchanged. */
  25% {
    opacity: 1;
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ----------------------------------------------------------- breakpoints
   Content-driven: the shelf's own spines wrap in rank order below their
   natural row width (handled by flex-wrap above); the column itself just
   narrows to fill a phone and stays capped at --content-width above it. */

@media (min-width: 640px) {
  .app-main {
    padding-left: 0;
    padding-right: 0;
  }
}

/* Scan and ISBN: a single 44px control on a phone doesn't need to widen,
   but the same page on a wide screen was just that column adrift on a
   blank field. Both pages opt in with .app-main--wide; every other page
   keeps the plain --content-width measure at every size. */
@media (min-width: 900px) {
  .app-main.app-main--wide {
    max-width: 900px;
  }

  .app-main--wide .wide-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-lg);
  }

  .app-main--wide .wide-main {
    flex: 3 1 0;
    min-width: 0;
  }

  .app-main--wide .wide-aside {
    flex: 2 1 0;
    min-width: 0;
  }
}

@media (pointer: coarse) {
  .btn, .pill, .toggle-option, .chip {
    min-height: var(--target-min);
  }
}

/* -------------------------------------------------------- easier to read
   PLAN 7h item 1: a second root attribute, stacked on any theme, never a
   theme itself (static/js/pages/you.js's "Easier to read" pill group).
   Ratios for every value below are checked by
   scratchpad/style_theme_contrast.py and recorded in
   review/contrast/paper-easier.md. */

:root[data-read="easier"] {
  /* A deeper ramp than Paper's: every fill clears 3:1 against paper and,
     since every step's label goes to paper text here (not just navy's),
     every label clears 7:1 against its own fill. .spine[data-step="..."]
     and .book-cover-fallback[data-step="..."] read these same five pairs,
     so no rule below them needs to change. */
  --share-orange: #7a2e12;
  --share-amber: #6b4210;
  --share-mustard: #5c4b0c;
  --share-teal: #1f4a4b;
  --share-navy: #22375c;
  --share-orange-text: var(--paper);
  --share-amber-text: var(--paper);
  --share-mustard-text: var(--paper);
  --share-teal-text: var(--paper);
  --share-navy-text: var(--paper);

  --target-min: 48px;
}

/* Body reads the scaled root size via 1rem (finish review fix 4), not a
   literal 18px - a literal would floor body copy at 18px even on a phone
   whose own root font is already larger than 16px, undercutting the size
   the reader asked their phone for. With the root's 112.5% below, a 16px
   phone still gets an 18px body and a 20px phone gets 22.5px. */
[data-read="easier"] body {
  font-size: 1rem;
}

:root[data-read="easier"] {
  font-size: 112.5%;
}

/* Spine titles read horizontally instead of climbing the spine, wrapping
   over up to four lines rather than truncating to one - overridden here,
   not duplicated, since .spine-label's own rule stays the vertical-text
   default for every other mode. */
:root[data-read="easier"] .spine-label {
  /* :root on the selector so this outranks a theme's own
     [data-theme="..."] .spine-label display-face rule, whatever the order. */
  writing-mode: horizontal-tb;
  transform: none;
  white-space: normal;
  text-align: left;
  /* The body face, a touch under the scaled 1rem: a theme's display face
     (Fraunces bold under Night Library) is wider than Cabin or Inter and
     broke "Emotional" mid-word at 18px in the 92px box; Inter or Cabin
     bold at 17px fits it whole and stays above the 16px standard label. */
  font-family: var(--font-body);
  font-size: 0.95rem;
  /* 4px at the sides, not 6px: "Emotional" at 18px bold is a hair wider
     than an 88px box and broke mid-word; 92px fits it whole. */
  padding: 6px 3px;
  /* A single long word ("Emotional" at 18px bold in an 88px box) must
     break rather than clip to one line with an ellipsis. */
  hyphens: auto;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
}

/* .spine's own min-height (120px, above) is too short for four wrapped
   18px lines (4 x 1.4 x 18px = 100.8px of content box) plus the label's
   6px padding on each side; 136px clears it with the border included
   (finish review fix 2), so the fourth line is never sliced off. */
[data-read="easier"] .spine {
  min-height: 136px;
  /* 2px sides, so the horizontal label keeps 88px of the 100px cell:
     "Emotional" in Inter bold at 17px measures 83px. */
  padding-left: 2px;
  padding-right: 2px;
}

/* The share word (always in the spine's accessible name - shelf.js's
   spineName) becomes visible text too, next to the percent. */
[data-read="easier"] .spine-share-word {
  display: inline;
}

/* Motion off: scroll-behavior for anything that later adds smooth
   scrolling, plus a universal rule so animation/transition stays off for
   everything under the attribute, not just today's two named sources
   (review finding 9) - the named rule stays too, since it is what
   out-specifies :root[data-read="easier"] .spine[data-step="..."] etc. */
:root[data-read="easier"] {
  scroll-behavior: auto;
}

[data-read="easier"] *,
[data-read="easier"] *::before,
[data-read="easier"] *::after {
  animation: none;
  transition: none;
}

[data-read="easier"] .spine,
[data-read="easier"] .spine.is-selected,
[data-read="easier"] .skeleton-pill {
  animation: none;
  transition: none;
}

/* ------------------------------------------------------- night library
   THEME_NIGHT_LIBRARY.md (PLAN 7h item 1, theme 1 of 5): near-black walnut
   ground, brass accent, cream ink, cloth-bound spines with a light top and
   dark foot, gilt percents, cream tag stickers - for literary fiction and
   evening browsing. Fonts self-hosted in static/css/themes/night-library.css,
   loaded only while this theme is active (theme.js's THEMES entry lists
   both families). Ramp label colours below are chosen by measured contrast
   against each fill (paper or ink, whichever clears 4.5:1) - see
   review/contrast/night-library.md for the full table, including why the
   choice differs from the accent/tint pair, which stay literal like
   Paper's (see the accent-vs-ramp split comment on :root above). Placed
   after :root and after the generic [data-read="easier"] block above so
   both plain-specificity ties resolve in this theme's favour. */
[data-theme="night-library"] {
  --paper: #17130F;
  --paper-grey: #241D16;
  --ink: #ECE1CC;
  --accent: #CDA35C;
  --accent-tint: #32291B;
  --link: #8FB4C9;

  --share-orange: #D9B36A;
  --share-amber: #C2853F;
  --share-mustard: #B37A52;
  --share-teal: #6E9490;
  --share-navy: #55609A;
  --share-orange-text: var(--paper);
  --share-amber-text: var(--paper);
  --share-mustard-text: var(--paper);
  --share-teal-text: var(--paper);
  --share-navy-text: var(--ink);

  --sticker-ground: #ECE1CC;
  --sticker-ink: #17130F;

  --font-body: "Inter", sans-serif;
  --font-display: "Fraunces", serif;

  /* rgba(17,17,17,.18) is invisible on walnut (finish review fix 5); a
     light rgba reads as a lift on the near-black ground instead. */
  --lift-shadow: rgba(236, 225, 204, 0.28);
}

/* Cloth-bound spines: a vertical gradient (light top, dark foot) stands in
   for the flat step fill, on both the labelled spine and the covers-mode
   foot band (.spine-cover-band) - both already read --spine-fill from the
   mechanism's own .spine[data-step="..."] rules, so no change there. */
[data-theme="night-library"] .spine,
[data-theme="night-library"] .spine-cover-band {
  /* Banded, not a full-height wash (finish review fix 1): only the 8px
     head and 24px foot padding carry the colour-mix wash. The text
     region (.spine's own content box, between --space-sm and
     calc(100% - --space-lg)) is flat spine-fill, so every rendered
     .spine-label glyph sits on the flat fill, not the wash - see
     review/contrast/night-library.md's flat-fill column. */
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--spine-fill, var(--share-navy)) 82%, white) 0,
    color-mix(in srgb, var(--spine-fill, var(--share-navy)) 82%, white) 3px,
    var(--spine-fill, var(--share-navy)) var(--space-sm),
    var(--spine-fill, var(--share-navy)) calc(100% - var(--space-lg)),
    color-mix(in srgb, var(--spine-fill, var(--share-navy)) 70%, black) 100%
  );
}

/* Gilt percents: the ranked number and the book page's big percent read
   the accent rather than the ramp step's own label colour. */
[data-theme="night-library"] .spine-percent,
[data-theme="night-library"] .book-percent {
  color: var(--accent);
}

/* Display type (Fraunces): the app name, page titles (shelf.js's
   #shelf-title is a .page-title too), the big percent on the book page,
   headings, door titles, shelf list names and spine titles (finish review
   fixes 2 and 3 - all three shelf comps and the home comp show these in
   Fraunces; home.js's door-card-title and shelf-item-name are spans, so
   the h1-h3 selector never reached them). Everything else (body copy,
   percents on the ramp fill, stickers and controls) stays --font-body
   (Inter). */
[data-theme="night-library"] h1,
[data-theme="night-library"] h2,
[data-theme="night-library"] h3,
[data-theme="night-library"] .app-name,
[data-theme="night-library"] .page-title,
[data-theme="night-library"] .book-percent,
[data-theme="night-library"] .door-card-title,
[data-theme="night-library"] .shelf-item-name,
[data-theme="night-library"] .spine-label {
  font-family: var(--font-display);
}

/* Easier to read on this ground (THEME_NIGHT_LIBRARY.md item 2): the
   generic :root[data-read="easier"] ramp above was measured on white
   paper and fails 3:1 against near-black ground, so this theme needs its
   own, lighter (not deeper) five steps - every fill still clears 3:1
   against ground and every label clears 7:1 against its own fill (both
   tighter than standard's 4.5:1), with neighbours kept visually distinct.
   Motion, scale, targets and horizontal titles all come from the generic
   [data-read="easier"] rules above and need no repeat here. Same plain
   attribute-selector specificity as the generic block, so this must stay
   after it in source order to win the tie - see review/contrast/
   night-library.md's Easier table. */
[data-theme="night-library"][data-read="easier"] {
  --share-orange: #D9B46B;
  --share-amber: #D4AA78;
  --share-mustard: #C59A7B;
  --share-teal: #93B0AD;
  --share-navy: #9AA0C3;
  --share-orange-text: var(--paper);
  --share-amber-text: var(--paper);
  --share-mustard-text: var(--paper);
  --share-teal-text: var(--paper);
  --share-navy-text: var(--paper);
}

/* ------------------------------------------------------- picture book
   THEME_PICTURE_BOOK.md (PLAN 7h item 1, theme 2 of 5): white ground,
   rounded, chunky, round badge stickers, one face throughout (Nunito) -
   for children's, young adult and romance readers. Font self-hosted in
   static/css/themes/picture-book.css, loaded only while this theme is
   active (theme.js's THEMES entry lists it). Ramp label colours below are
   chosen by measured contrast against each fill (ink or white, whichever
   clears 4.5:1) - see review/contrast/picture-book.md for the full table.
   The mock's 112px cell and larger spine-label size are not carried over
   (THEME_PICTURE_BOOK.md's "Rules that differ from the mock"): the cell
   stays --spine-cell-width: 100px and .spine-label keeps its standard
   size, both already set once in :root and untouched here. */
[data-theme="picture-book"] {
  --paper: #FFFFFF;
  --paper-grey: #F4F0FB;
  --ink: #2E2A3A;
  --accent: #BA3820;
  --accent-tint: #F5E1DE;
  --link: #4558D6;

  --share-orange: #C33F27;
  --share-amber: #C28812;
  --share-mustard: #2F6B25;
  --share-teal: #19647A;
  --share-navy: #6C5AC7;
  --share-orange-text: var(--paper);
  --share-amber-text: var(--ink);
  --share-mustard-text: var(--paper);
  --share-teal-text: var(--paper);
  --share-navy-text: var(--paper);

  --sticker-ground: #FFFFFF;
  --sticker-ink: var(--accent);

  --font-body: "Nunito", sans-serif;
  --font-display: "Nunito", sans-serif;

  --radius: 12px;
  --border: 2px solid var(--ink);
  --border-dashed: 2px dashed var(--ink);

  /* rgba from this theme's own ink (#2E2A3A), same pattern as Paper's own
     rgba(17,17,17,.18) drawn from its ink. */
  --lift-shadow: rgba(46, 42, 58, 0.18);
}

/* Fully rounded badge stickers (STYLE_THEMES.md section 5): --radius above
   is 12px, shared by spines, buttons, pills, cards and inputs, but the
   sticker itself wants a full pill regardless of that shared value, so
   this overrides border-radius alone. Ground and ink already come from
   the shared .sticker rule reading --sticker-ground/--sticker-ink (set
   above, ink pointed at --accent so the border and text read as one
   accent-coloured badge); the border width is widened to match this
   theme's own 2px border weight. */
[data-theme="picture-book"] .sticker {
  border-radius: 999px;
  border-width: 2px;
  padding: 2px 6px;
}

/* Finish review fixes (review/theme-picture-book-finish.md, Material fixes
   1, 4 and 5): a handful of dividers and step borders stay at literal
   pixel widths that don't track --border, so this theme's own 2px/4px
   promise breaks at those spots. Each override routes through --border
   where its value (2px solid ink) already matches; the chosen-segment
   ring and the two structure steps have no matching token, so they stay
   explicit. */
[data-theme="picture-book"] .toggle-option {
  border-right: var(--border);
}

[data-theme="picture-book"] .toggle-option.is-selected {
  box-shadow: inset 0 0 0 2px var(--accent);
}

[data-theme="picture-book"] .shelf-item,
[data-theme="picture-book"] .picker-option {
  border-bottom: var(--border);
}

[data-theme="picture-book"] .spine.is-selected {
  border-width: 4px;
  outline-width: 4px;
}

[data-theme="picture-book"] .empty-state {
  border-top-width: 4px;
}

/* Fix 3: comp lettering sits at the heavy end of Nunito; the 100px cell
   rule keeps size fixed, so weight is the only lever. .spine-label stays
   at 700 so the measured Easier-mode "Emotional" width still holds. */
[data-theme="picture-book"] h1,
[data-theme="picture-book"] h2,
[data-theme="picture-book"] .app-name,
[data-theme="picture-book"] .page-title,
[data-theme="picture-book"] .door-card-title,
[data-theme="picture-book"] .shelves-heading,
[data-theme="picture-book"] .shelf-item-name,
[data-theme="picture-book"] .book-percent {
  font-weight: 800;
}

/* Easier to read needs no ramp sub-block here (THEME_PICTURE_BOOK.md's
   "Easier to read" section): the mechanism's generic :root[data-read=
   "easier"] ramp was measured on white and this theme's own ground is
   also white, so it already clears the same 3:1 fill / 7:1 label minimums
   - confirmed in review/contrast/picture-book.md. Ink on ground (13.92:1)
   and link on ground (5.80:1) both clear what Easier asks of them too, so
   neither needs its own override. The 2px border also leaves the Easier
   spine label 86px of the 100px cell (2px border either side, the
   [data-read="easier"] .spine rule's own 2px side padding, the label's
   own 3px side padding); measured in a live canvas at 2026-09-22, Nunito
   bold "Emotional" at 17px is 80.3px wide, which fits without widening
   the padding further. */

/* ------------------------------------------------------- circuit
   THEME_CIRCUIT.md (PLAN 7h item 1, theme 3 of 5): dark blue-black readout,
   monospace, a luminous status ramp - for science fiction and technology
   readers. Font self-hosted in static/css/themes/circuit.css, loaded only
   while this theme is active (theme.js's THEMES entry lists it). Ramp
   label colours below are chosen by measured contrast against each fill:
   --ink (#D7E4EE) never clears 4.5:1 on any of the five bright fills (best
   case 3.22:1, on navy), so every step's label is --paper (the ground,
   #0B0F1A) instead - the weakest fill/label pair (navy) still measures
   4.60:1, confirming STYLE_THEMES.md's own figure - see
   review/contrast/circuit.md for the full table. --border and
   --border-dashed already reach the header rule (.app-header's
   border-bottom), the action bar rule (.action-bar's border-top) and the
   shelf line (.median-divider's border-top) without a repeat here, since
   all three already read var(--border)/var(--border-dashed). */
[data-theme="circuit"] {
  --paper: #0B0F1A;
  --paper-grey: #121A2B;
  --ink: #D7E4EE;
  --accent: #3FE0C5;
  --accent-tint: #132E34;
  --link: #4FD1FF;

  --share-orange: #33E08A;
  --share-amber: #38C6E0;
  --share-mustard: #E0C93F;
  --share-teal: #E08A33;
  --share-navy: #E0433F;
  --share-orange-text: var(--paper);
  --share-amber-text: var(--paper);
  --share-mustard-text: var(--paper);
  --share-teal-text: var(--paper);
  --share-navy-text: var(--paper);

  /* Sticker ground: the approved comp samples #121A2B, this theme's
     --paper-grey, not the page ground --paper (#0B0F1A) - finish-review
     fix 4, pixel sample outranks the spec's "paper" wording on material.
     Ink re-measured on this ground in review/contrast/circuit.md. */
  --sticker-ground: var(--paper-grey);
  --sticker-ink: var(--ink);

  --font-body: "JetBrains Mono", monospace;
  --font-display: "JetBrains Mono", monospace;

  --radius: 0px;
  --border: 1px solid var(--accent);
  --border-dashed: 1px dashed var(--accent);

  /* A light rgba drawn from this theme's own ink, same pattern as Night
     Library's - a dark rgba is invisible on a near-black ground. */
  --lift-shadow: rgba(215, 228, 238, 0.28);
}

/* Percents in the accent (THEME_CIRCUIT.md identity), on both the ranked
   spine and the book page's own big percent. */
[data-theme="circuit"] .spine-percent,
[data-theme="circuit"] .book-percent {
  color: var(--accent);
}

/* Chosen pills and toggles (finish-review pattern from Night Library):
   accent text on the accent tint measures 8.64:1 here, well clear of
   4.5:1, and matches the approved mock (circuit-shelf-spines-390.png's
   "For you"/"Spines" toggle) - the shared rule above keeps ink for every
   theme that does not override it. */
[data-theme="circuit"] .pill.is-selected,
[data-theme="circuit"] .toggle-option.is-selected {
  color: var(--accent);
}

/* CAPS voice (THEME_CIRCUIT.md identity, finish-review fix 1): the app
   name, every page title (including the date title and the shelf name,
   both .page-title), the two home door-card titles and the "Your
   shelves" heading all render in caps with a touch of tracking in the
   three approved comps. ".shelf-heading" ("Not enough shelvings") is a
   different, singular class and stays mixed case, matching the mock. */
[data-theme="circuit"] .app-name,
[data-theme="circuit"] .page-title,
[data-theme="circuit"] .door-card-title,
[data-theme="circuit"] .shelves-heading {
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Stickers: an accent border rather than the shared rule's ink border
   (--sticker-ink stays ink for the text), plus bracket marks flanking the
   tag - drawn as empty pseudo-elements (content: "", never a literal "["
   or "]") so the generated marks never reach the sticker's accessible
   name via its text content. Each bracket is a 2px accent side border
   with short top and bottom bars, mirrored left and right. */
[data-theme="circuit"] .sticker {
  position: relative;
  border-color: var(--accent);
  padding-left: 10px;
  padding-right: 10px;
}

[data-theme="circuit"] .sticker::before,
[data-theme="circuit"] .sticker::after {
  content: "";
  position: absolute;
  top: 2px;
  bottom: 2px;
  width: 6px;
  border-top: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
}

[data-theme="circuit"] .sticker::before {
  left: 2px;
  border-left: 2px solid var(--accent);
}

[data-theme="circuit"] .sticker::after {
  right: 2px;
  border-right: 2px solid var(--accent);
}

/* Monospace is wide (THEME_CIRCUIT.md "Monospace and the 100px cell"): a
   touch smaller and tighter than the shared 1rem/700 default so titles
   truncate less than the mock's own "The spark of li..." - measured at
   2026-09-22 with a live canvas (JetBrains Mono bold), 10 characters fit
   the 88px available run (.spine's 120px min-height less its 8px top and
   24px bottom padding) at 0.9rem/-0.02em, against 9 at the shared
   1rem/no-tracking default - see review/theme-circuit-report.md. */
[data-theme="circuit"] .spine-label {
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* Easier to read on this ground (THEME_CIRCUIT.md "Easier to read ramp"):
   the generic :root[data-read="easier"] ramp above was measured on white
   paper and fails 3:1 against near-black ground, so this theme needs its
   own five steps. Three (orange, amber, mustard) already clear 3:1 fill
   and 7:1 label unmodified (their standard values are bright enough);
   teal and navy are lightened towards white until their own label
   (--paper on the fill) clears 7:1 with a safety margin - see
   review/contrast/circuit.md's Easier table. Same plain attribute-selector
   specificity as the generic block, so this must stay after it in source
   order to win the tie (Night Library's own comment explains the same
   tie). */
[data-theme="circuit"][data-read="easier"] {
  --share-orange: #33E08A;
  --share-amber: #38C6E0;
  --share-mustard: #E0C93F;
  --share-teal: #E39647;
  --share-navy: #EB8582;
  --share-orange-text: var(--paper);
  --share-amber-text: var(--paper);
  --share-mustard-text: var(--paper);
  --share-teal-text: var(--paper);
  --share-navy-text: var(--paper);
}

/* THEME_CIRCUIT.md "Monospace and the 100px cell": measured with a live
   canvas at 2026-09-22, JetBrains Mono bold "Emotional" is 82.1px at the
   shared Easier size (0.95rem) and 73.4px at this theme's own narrower
   0.85rem - both already clear the 88px box, unlike the spec's own ~93px
   estimate (recorded as a contradiction in review/theme-circuit-report.md
   rather than silently picked); the override is kept regardless, since
   THEME_CIRCUIT.md asks for it outright and the extra margin only helps.
   Same specificity as the generic easier label rule, so it must stay
   after it in source order to win the tie, same reasoning as the ramp
   block above. */
[data-theme="circuit"][data-read="easier"] .spine-label {
  font-size: 0.85rem;
}

/* ------------------------------------------------------- pulp
   THEME_PULP.md (PLAN 7h item 1, theme 4 of 5): aged cream paper, halftone
   dots, rubber-stamp flags - for crime and thrillers. Fonts self-hosted in
   static/css/themes/pulp.css, loaded only while this theme is active
   (theme.js's THEMES entry lists both families). Ramp label colours below
   are chosen by measured contrast against each fill: --ink (#241B12) never
   clears 4.5:1 on any of the five fills (every one is itself a dark tone,
   best case 2.28:1 on orange), so every step's label is --paper (the
   ground, #EDE3C8) instead - the weakest fill/label pair (orange) still
   measures 5.80:1, confirming STYLE_THEMES.md's own "5.8 or better" figure
   - see review/contrast/pulp.md for the full table. */
[data-theme="pulp"] {
  --paper: #EDE3C8;
  --paper-grey: #E3D6AE;
  --ink: #241B12;
  --accent: #A8182A;
  --accent-tint: #E3C5B0;
  --link: #1E4FA6;

  --share-orange: #A8182A;
  --share-amber: #7A3D10;
  --share-mustard: #6B4A0A;
  --share-teal: #155F53;
  --share-navy: #2C2E7A;
  --share-orange-text: var(--paper);
  --share-amber-text: var(--paper);
  --share-mustard-text: var(--paper);
  --share-teal-text: var(--paper);
  --share-navy-text: var(--paper);

  /* Sticker ground (finish note, review/theme-pulp-report.md): THEME_PULP.md
     names --sticker-ground: transparent, but --accent (#A8182A) is the
     identical hex used for --share-orange, so a transparent sticker over an
     orange-step spine would put accent-coloured text and border on an
     accent-coloured fill - 1.00:1, unreadable - and every other step's fill
     is dark enough that accent text on it still fails 3:1 (worst case
     1.59:1 on navy). The approved mock (pulp-shelf-spines-390.png) shows
     every stamped sticker on a pale tan backing distinct from the spine
     colour behind it, matching this theme's own --accent-tint exactly - the
     same "pixel sample outranks the spec's prose" precedent as Circuit's
     own sticker-ground fix (review/contrast/circuit.md). --sticker-ink
     stays the accent, so the border and text are unaffected. */
  --sticker-ground: var(--accent-tint);
  --sticker-ink: var(--accent);

  --font-body: "Courier Prime", monospace;
  --font-display: "Anton", sans-serif;

  /* Finish review fix 1: Anton ships one weight (400). Every display
     element below still asks for 700, so without this the browser
     synthesises a fake bold over Anton's own stroke; this stops that,
     and the display rule further down asks for 400 instead. Courier
     Prime keeps its real 700 file, unaffected. */
  font-synthesis-weight: none;

  --radius: 2px;
  --border: 2px solid var(--ink);
  --border-dashed: 2px dashed var(--ink);

  /* rgba from this theme's own ink (#241B12) at 18%, same pattern as
     Paper's and Picture Book's own rgba - a dark rgba reads as a lift on
     this light cream ground. */
  --lift-shadow: rgba(36, 27, 18, 0.18);
}

/* Faint halftone dot over the whole page (THEME_PULP.md identity): a 6px
   tile of small ink dots at low alpha, so the plain ground still measures
   13.24:1 for body ink against it (review/contrast/pulp.md) - the dot
   layer itself blends to about #D5CBB2 against the #EDE3C8 ground, a
   1.26:1 non-text difference, well below anything that could compete with
   glyph strokes. Removed under Easier to read (a clean ground for large
   text, THEME_PULP.md's own Easier section). */
[data-theme="pulp"] body {
  background-image: radial-gradient(rgba(36, 27, 18, 0.12) 0.6px, transparent 0.7px);
  background-size: 6px 6px;
}

[data-theme="pulp"][data-read="easier"] body {
  background-image: none;
}

/* Display type (Anton): the app name, page titles (shelf.js's #shelf-title
   is a .page-title too), headings, door titles and shelf list names - the
   same element list as Night Library's own display-face rule. Spine titles
   also read Anton, and are additionally set in capitals below (the mock's
   own "THE SPARK OF LIFE"), which the other themes' spine titles are not.
   Everything else (body copy, percents, stickers, controls) stays
   --font-body (Courier Prime). */
[data-theme="pulp"] h1,
[data-theme="pulp"] h2,
[data-theme="pulp"] h3,
[data-theme="pulp"] .app-name,
[data-theme="pulp"] .page-title,
[data-theme="pulp"] .door-card-title,
[data-theme="pulp"] .shelf-item-name,
[data-theme="pulp"] .spine-label {
  font-family: var(--font-display);
  /* Anton has one weight (400); the shared rules for these elements all
     ask for 700 (h1-h3 by browser default, the rest by their own rule),
     which is why font-synthesis-weight above is needed too. */
  font-weight: 400;
}

/* Spine titles in capitals, as the approved mock shows (both the labelled
   spine and the covers-mode fallback text share this one class). */
[data-theme="pulp"] .spine-label {
  text-transform: uppercase;
}

/* Stamped, outlined stickers (THEME_PULP.md identity): a 2px accent border
   (the shared .sticker rule's own 1px, from --sticker-ink) and uppercase
   text - already Courier Prime via --font-body, so only the case changes
   here. */
[data-theme="pulp"] .sticker {
  border-width: 2px;
  text-transform: uppercase;
}

/* A small alternating rotation by nth-child parity (THEME_PULP.md
   identity): static, not animated - no animation or transition property
   appears in this file's pulp rules. Extended to the book page's own
   sticker row (finish review fix 6: the spec's rotation promise isn't
   scoped to the shelf). Removed under Easier to read below. */
[data-theme="pulp"] .stickers .sticker:nth-child(odd),
[data-theme="pulp"] .detail-stickers .sticker:nth-child(odd) {
  transform: rotate(-2deg);
}

[data-theme="pulp"] .stickers .sticker:nth-child(even),
[data-theme="pulp"] .detail-stickers .sticker:nth-child(even) {
  transform: rotate(2deg);
}

/* A plain [data-theme="pulp"][data-read="easier"] .sticker reset here
   loses to the nth-child rules above (0,0,4,0 beats its own 0,0,3,0,
   regardless of source order), which is why the tilt still showed under
   Easier to read. Matching the nth-child selectors' own shape brings this
   to equal specificity, and it comes later in the file, so it wins. */
[data-theme="pulp"][data-read="easier"] .stickers .sticker,
[data-theme="pulp"][data-read="easier"] .detail-stickers .sticker {
  transform: none;
}

/* Easier to read on this ground (THEME_PULP.md's own instruction: measure
   the mechanism's deeper ramp against #EDE3C8 and only override a step
   that fails 3:1 fill/ground or 7:1 label/fill). Three of the five already
   clear both minimums unmodified against this cream ground (orange 7.37,
   teal 7.68, navy 9.28 - all well past 7.0) and so are not repeated here;
   amber (6.81) and mustard (6.66) both fall short of the 7:1 label minimum
   by a small margin and are darkened further until they clear it with a
   safety margin - see review/contrast/pulp.md for the full working. Their
   text tokens are not repeated: the generic :root[data-read="easier"]
   block already points every step's label at var(--paper), which resolves
   to this theme's own cream ground here. */
[data-theme="pulp"][data-read="easier"] {
  --share-amber: #673F0F;
  --share-mustard: #56460B;
}

/* THEME_PULP.md "Easier to read": the label uses the body face here
   (Courier Prime, monospace), same shape as Circuit's own narrower label
   size under Easier to read. Uppercase is this theme's own spine-label
   rule further up, not the spec's Easier section, so it's turned off
   here: text-transform: none, decided over keeping caps (finish review
   fix 4). Measured: "Emotional" at 0.85rem (13.6px) Courier Prime bold,
   monospace's ~0.6em advance per glyph, is 9 chars * 8.16px = about 73px,
   against the 78px content box (88px minus 2px borders and 3px padding
   each side) - fits with roughly 5px to spare. See
   review/theme-pulp-report.md. */
[data-theme="pulp"][data-read="easier"] .spine-label {
  font-size: 0.85rem;
  text-transform: none;
}

/* Finish review fixes (review/theme-pulp-finish.md, Material fixes 2 and
   3): the same handful of dividers and step borders that needed an
   override in Picture Book (app.css:1663-1680) stay at literal pixel
   widths here too, breaking this theme's own 2px/4px promise. Each
   override routes through --border where its value (2px solid ink)
   already matches; the chosen-segment ring and the two structure steps
   have no matching token, so they stay explicit. */
[data-theme="pulp"] .toggle-option {
  border-right: var(--border);
}

[data-theme="pulp"] .toggle-option.is-selected {
  box-shadow: inset 0 0 0 2px var(--accent);
}

[data-theme="pulp"] .picker-option {
  border-bottom: var(--border);
}

[data-theme="pulp"] .spine.is-selected {
  border-width: 4px;
  outline-width: 4px;
}

[data-theme="pulp"] .empty-state {
  border-top-width: 4px;
}

/* Finish review fix 5: the approved mock shows accent text on the tint
   for a chosen pill or toggle, but the shared rule (app.css:667-676)
   ships ink instead. THEME_PULP.md line 14 ("Chosen pills and toggles:
   ink on the tint with the accent border") authorises this, and ink on
   --accent-tint measures about 10.4:1 here (WCAG relative luminance,
   #241B12 on #E3C5B0) - comfortably clear, unlike accent-on-tint, which
   is why ink stays. No override needed: this is a note, not a rule. */

/* ------------------------------------------------------- folio
   THEME_FOLIO.md (PLAN 7h item 1, theme 5 of 5): off-white ground, black
   ink, one red accent, gilt-lettered spines, one face throughout (EB
   Garamond) - for history and classics readers, and the default theme
   since 2026-09-24. Font self-hosted in static/css/themes/folio.css,
   loaded only while this theme is active: by index.html's first-paint
   script when folio resolves, by theme.js's applyTheme when picked
   in-session (theme.js's THEMES entry lists it), and precached in sw.js's
   APP_SHELL. The ramp fills below are
   STYLE_THEMES.md's rebuilt figures: its own text records that the first
   capture read as a narrow-valued, muddy block of browns and was rebuilt
   wider with a brighter gilt - see review/contrast/folio.md for the full
   table. */
[data-theme="folio"] {
  --paper: #F5F1E6;
  --paper-grey: #EAE3D2;
  --ink: #1B1815;
  --accent: #A3202B;
  --accent-tint: #E9D2CA;
  --link: #2A4B7C;

  /* Fixed gilt for spine lettering (THEME_FOLIO.md identity): unlike every
     token above, this one never varies by ramp step - all five
     --share-*-text tokens below point straight at it, so every spine title
     reads the same gilt colour regardless of which share band it sits on. */
  --gilt: #EFD9A0;

  --share-orange: #7A2230;
  --share-amber: #6B3D1E;
  --share-mustard: #585020;
  --share-teal: #333A4C;
  --share-navy: #100F0D;
  --share-orange-text: var(--gilt);
  --share-amber-text: var(--gilt);
  --share-mustard-text: var(--gilt);
  --share-teal-text: var(--gilt);
  --share-navy-text: var(--gilt);

  /* Sticker ground (THEME_FOLIO.md's own instruction: "measure the accent
     against the spine fills it sits on ... and against the ground; if any
     pairing is under 4.5:1, give the sticker a ground-coloured backing
     plate and record it"). The spec's literal --sticker-ground: transparent
     would put accent-red sticker text straight onto whichever ramp fill
     sits behind the spine's foot band; measured (review/contrast/folio.md)
     that pairing fails badly on every step (1.08:1 on mustard up to 2.55:1
     on navy, all well under 4.5:1). A solid backing in the theme's own
     ground colour restores accent-on-ground's 6.65:1 regardless of which
     step the sticker sits over - same "measure, then deviate and record"
     precedent as Pulp's own sticker-ground fix (review/contrast/pulp.md).
     --sticker-ink stays the accent, so the border removal below is
     unaffected. */
  --sticker-ground: var(--paper);
  --sticker-ink: var(--accent);

  --font-body: "EB Garamond", serif;
  --font-display: "EB Garamond", serif;

  --radius: 2px;
  --border: 1px solid var(--ink);
  --border-dashed: 1px dashed var(--ink);

  /* rgba from this theme's own ink (#1B1815) at 18%, same pattern as every
     other built theme's own lift shadow. */
  --lift-shadow: rgba(27, 24, 21, 0.18);
}

/* Unboxed stickers (THEME_FOLIO.md identity): no border, small caps text
   with a touch of extra tracking so the capitals read as a label rather
   than a shout. Ground and ink already come from the shared .sticker rule
   reading --sticker-ground/--sticker-ink (set above); this removes the
   1px border the shared rule draws from --sticker-ink and adds the case
   and spacing. */
[data-theme="folio"] .sticker {
  border: none;
  font-variant-caps: small-caps;
  letter-spacing: 0.04em;
}

/* True small caps (THEME_FOLIO.md identity): the pill groups' labels, the
   section headings and the You chip, alongside the sticker rule above.
   font-variant-caps renders real small-capital glyphs from the face's own
   font data rather than a synthetic text-transform: uppercase, so
   descenders and numerals keep their normal case forms. .field-label
   covers every pill group's label (and the sync-code text input's own
   label, which reads fine in small caps too); h2 covers every section
   heading on the You page plus the home page's "Your shelves"
   (.shelves-heading is itself an h2). */
[data-theme="folio"] .field-label,
[data-theme="folio"] h2,
[data-theme="folio"] .chip,
[data-theme="folio"] .toggle-option,
[data-theme="folio"] .pill,
[data-theme="folio"] .door-card-title {
  font-variant-caps: small-caps;
}

/* Chosen pills and toggles in the accent on the tint, as the approved
   comp shows (finish review: 5.19:1 measured, review/contrast/folio.md),
   in place of the mechanism's ink-on-tint default. */
[data-theme="folio"] .pill.is-selected,
[data-theme="folio"] .toggle-option.is-selected {
  color: var(--accent);
}

/* Percents in ink, in the one face, tabular figures (THEME_FOLIO.md
   identity). Ink and the one face already hold by inheritance - neither
   element sets its own colour or font-family, so both already read --ink
   in --font-body (EB Garamond) once the block above is active; only the
   tabular-figure request needs an explicit rule. */
[data-theme="folio"] .spine-percent,
[data-theme="folio"] .book-percent {
  font-variant-numeric: tabular-nums;
}

/* Easier to read (THEME_FOLIO.md's own instruction: measure the
   mechanism's deep ramp against this theme's off-white #F5F1E6 ground and
   add an override only for a step that fails). Measured
   (review/contrast/folio.md): every one of the five generic Easier fills
   clears 7:1 against #F5F1E6 unmodified (mustard is weakest at 7.54:1), so
   no [data-theme="folio"][data-read="easier"] ramp block is needed here.
   Labels under the mode come from the mechanism, not this theme's own
   gilt: the generic :root[data-read="easier"] block's --share-*-text:
   var(--paper) rules (paper resolving to this theme's own #F5F1E6 ground)
   outrank the plain [data-theme="folio"] block above by specificity
   regardless of source order, so gilt lettering automatically steps aside
   for paper-on-fill under Easier to read. */
