/* ==========================================================================
   Open Shelf - global stylesheet
   --------------------------------------------------------------------------
   Every rule in the web section lives here. Pages carry no <style> blocks and
   no inline style attributes; markup composes the classes below. All values
   come from tokens.css, so nothing here hard-codes a colour, size or timing.

   Contents
     1.  Reset and base elements
     2.  Layout primitives
     3.  Typography helpers
     4.  Header, nav, footer
     5.  Buttons and controls
     6.  Form fields
     7.  Chips, pills, tags
     8.  Generated book covers
     9.  Book cards and grids
     10. Rating display
     11. Home page
     12. Book list page
     13. Book details page
     14. Profile page
     15. Feedback states - loading, empty, error
     16. Utilities and accessibility
   ========================================================================== */

/* ------------------------------------------------- 1. Reset and base ----- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  /* dvh tracks the shrinking viewport behind a mobile URL bar; the vh line is
     the fallback for browsers without dynamic viewport units. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background-color: var(--color-canvas);
  color: var(--color-text);
  font-family: var(--os-font-body);
  font-size: var(--os-text-md);
  line-height: var(--os-leading-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* The brand keeps large surfaces flat: no gradients, textures or background
   imagery. The canvas is the plain light-gray token. */

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-family: var(--os-font-display);
  font-weight: var(--os-weight-semibold);
  line-height: var(--os-leading-tight);
  letter-spacing: var(--os-tracking-tight);
  color: var(--color-text);
  /* Book titles run long and arrive unhyphenated; at 360px a single
     unbreakable word would otherwise push the page sideways. */
  overflow-wrap: break-word;
}

p {
  margin: 0;
}

a {
  color: var(--color-accent);
  text-decoration-color: var(--color-accent-border);
  text-underline-offset: 0.18em;
  transition: color var(--os-duration-fast) var(--os-ease-out);
}

a:hover {
  color: var(--color-accent-hover);
  text-decoration-color: currentColor;
}

img {
  max-width: 100%;
  display: block;
}

ul, ol {
  margin: 0;
  padding: 0;
}

li {
  list-style: none;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

::selection {
  background: var(--color-selection);
  color: var(--color-text);
}

:focus-visible {
  outline: var(--os-border-thick) solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--os-radius-xs);
}

/* ------------------------------------------------ 2. Layout primitives --- */

.container {
  width: 100%;
  max-width: var(--os-container-lg);
  margin-inline: auto;
  padding-inline: var(--os-gutter);
}

.container--md { max-width: var(--os-container-md); }
.container--sm { max-width: var(--os-container-sm); }

/* Tighter vertical rhythm on a phone, where every 16px above the fold is paid
   for by the reader; the designed spacing comes back from tablet up. */
.page {
  flex: 1 0 auto;
  padding-block: var(--os-space-6) var(--os-space-8);
  position: relative;
  z-index: 1;
}

@media (min-width: 40.0625rem) {
  .page { padding-block: var(--os-space-8) var(--os-space-10); }
}

/* A page whose own sections run edge to edge. It drops the page padding and
   the container, and each section brings its own .container back. Used by the
   book detail page, where the hero, the cautions and the club band are full
   width bands of colour. */
.page--bleed {
  padding-block: 0;
}

.section {
  margin-block-start: var(--os-space-9);
}

.section__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--os-space-3);
  margin-block-end: var(--os-space-5);
}

/* The head that carries a legend or a caveat on its far edge: the two sides
   align on their last line rather than their first. */
.section__head--split { align-items: flex-end; }

.section__title {
  font-size: var(--os-text-2xl);
}

/* What the corpus settled and what it did not, one under the other. Two
   claims, not one sentence: run together they read as a single thought and
   the contrast the section is built on disappears. Block spans rather than
   two headings, because it is still one heading for the section - the
   line-height carries the rhythm without a wrapper. */
.section__title-line {
  display: block;
}

/* The word that says which claim the line is, in the colour of the end of the
   seam it belongs to: what the corpus settled reads teal, what it argued over
   reads coral. The rest of each line stays ink, so the pair of leads is the
   only thing carrying colour at heading size. */
.section__title-lead {
  color: var(--color-support);
}

.section__title-lead--contested {
  color: var(--color-accent);
}

/* The half the corpus never filled. It holds the heading's second line so the
   section is not lopsided over two sides, and it drops to subtle ink with no
   colour on its lead: nothing happened on this axis, so nothing lights up. */
.section__title-line--absent,
.section__title-line--absent .section__title-lead {
  color: var(--color-text-subtle);
}

/* A "?" beside a label, and the explanation it opens.
   ------------------------------------------------------------------------
   Methodology that used to sit permanently beside a heading. It is worth
   stating exactly once and reading once, so it moves behind a control: the
   cards stop competing with a paragraph about the rules that produced them.

   The toggle inherits the label's colour, so a "?" on a teal eyebrow is teal
   and one on a navy band is whatever that band's ink is. The popover resets
   the label's type wholesale - it is set inside an uppercase, letter-spaced,
   semibold eyebrow and has to read as body copy. */
.info {
  position: relative;
  display: inline-flex;
  vertical-align: middle;
}

.info__toggle {
  display: grid;
  place-items: center;
  width: 1.125rem;
  height: 1.125rem;
  padding: 0;
  border: var(--os-border-hair) solid currentColor;
  border-radius: var(--os-radius-pill);
  background: none;
  color: inherit;
  font-family: inherit;
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-bold);
  line-height: 1;
  letter-spacing: 0;
  cursor: pointer;
  opacity: 0.75;
  transition: opacity var(--os-duration-fast) var(--os-ease-out),
    background var(--os-duration-fast) var(--os-ease-out);
}

.info__toggle:hover,
.info__toggle[aria-expanded="true"] {
  opacity: 1;
  background: var(--color-support-soft);
}

.info__popover {
  position: absolute;
  inset-block-start: calc(100% + var(--os-space-2));
  inset-inline-start: 50%;
  z-index: var(--os-z-overlay);
  /* Anchored to the "?" but never past the viewport, on a heading that sits
     near the left edge on a phone. */
  translate: -50%;
  width: max-content;
  max-width: min(24rem, calc(100vw - 2 * var(--os-gutter)));
  padding: var(--os-space-4);
  background: var(--color-surface-raised);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-md);
  box-shadow: var(--os-shadow-lg);
  color: var(--color-text-muted);
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-regular);
  line-height: var(--os-leading-relaxed);
  letter-spacing: var(--os-tracking-normal);
  text-transform: none;
  text-align: start;
  white-space: normal;
}

.section__foot {
  margin-block-start: var(--os-space-4);
  font-size: var(--os-text-sm);
  color: var(--color-text-subtle);
  max-width: 78ch;
}

.section__link {
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-medium);
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-4);
}

.stack--tight { gap: var(--os-space-2); padding-bottom: var(--os-space-4); }
.stack--loose { gap: var(--os-space-6); }

.row {
  display: flex;
  align-items: center;
  gap: var(--os-space-3);
  flex-wrap: wrap;
}

.divider {
  height: var(--os-border-hair);
  background: var(--color-divider);
  border: 0;
  margin-block: var(--os-space-5);
}

@media (min-width: 40.0625rem) {
  .divider { margin-block: var(--os-space-6); }
}

/* --------------------------------------------- 3. Typography helpers ----- */

/* Small tracked section labels are teal; ALL-CAPS is reserved for these and
   for the tagline, which is the one coral caps treatment. */
.eyebrow {
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--color-support);
}

.eyebrow--tagline {
  color: var(--color-accent);
  letter-spacing: var(--os-tracking-tagline);
}

/* Teal at full strength disappears into the navy bands; the tint from the
   brand's teal ramp is the on-inverse label colour. */
.eyebrow--on-dark { color: var(--os-teal-200); }

/* A section's own label is structure, not signal. In ink it names the section
   and gets out of the way; in teal it competed with the coloured leads in the
   heading right under it and with the teal the sections use to mean something.
   The "?" toggle inherits the label's colour, so it comes with it. */
.section .eyebrow:not(.eyebrow--on-dark),
.cautions .eyebrow:not(.eyebrow--on-dark) {
  color: var(--color-text);
}

.lede {
  font-size: var(--os-text-lg);
  line-height: var(--os-leading-relaxed);
  color: var(--color-text-muted);
  max-width: 60ch;
}

.muted { color: var(--color-text-muted); }
.subtle { color: var(--color-text-subtle); }

.meta {
  font-size: var(--os-text-sm);
  color: var(--color-text-muted);
}

.numeric {
  font-variant-numeric: tabular-nums;
}

.prose {
  font-size: var(--os-text-lg);
  line-height: var(--os-leading-relaxed);
  color: var(--color-text-muted);
  max-width: 68ch;
  overflow-wrap: break-word;
}

.prose p + p {
  margin-block-start: var(--os-space-4);
}

.clamp-1,
.clamp-2,
.clamp-3,
.clamp-4 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.clamp-1 { -webkit-line-clamp: 1; line-clamp: 1; }
.clamp-2 { -webkit-line-clamp: 2; line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; line-clamp: 3; }
.clamp-4 { -webkit-line-clamp: 4; line-clamp: 4; }

/* ------------------------------------- 4. Header, nav and site footer ---- */

/* Design direction 3b, "the command surface". The nav detaches from the page
   edge and floats as its own object, so the page - a warm hero, a wall of
   jackets - runs edge to edge behind it. Search stops being a text field in a
   bar and becomes a command entry with a `/` hint that opens the palette in
   section 4b.

   Three house rules bend the direction to this site rather than the mock:
   corners stay at 8px (tokens.css caps the scale - no stadiums), the capsule
   is opaque (the brand does not use transparency or blur), and coral stays
   rationed to the one active marker on the mobile bar.

   The header is still sticky and still in flow, so it holds its own height at
   rest and no full-bleed hero starts underneath it. It floats over the page
   only once the page scrolls, which is when floating is worth the shadow. */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--os-z-header);
  padding-block: var(--os-space-3);
  /* The header's box runs the full width but only the capsule is a surface.
     Without this the transparent gutter either side would swallow clicks
     meant for the page scrolling under it. */
  pointer-events: none;
}

/* The container the capsule is inset within: it keeps the site's gutter, and
   caps narrower than the page so the capsule reads as an object on the page
   rather than a bar spanning it. */
.site-header__inner {
  max-width: var(--os-container-md);
}

.capsule {
  pointer-events: auto;
  min-height: var(--os-header-height);
  display: flex;
  align-items: center;
  gap: var(--os-space-2);
  padding-inline: var(--os-space-4);
  background: var(--color-surface-raised);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
  box-shadow: var(--os-shadow-lg);
}

/* The brand is the logo artwork - the shelf mark and the wordmark set as one
   lockup - not an image beside live type. Nothing here sets a font, because
   there is no text left in it to set. */
.brand {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  text-decoration: none;
}

/* Height-led: the intrinsic width and height are on the <img> so the box is
   reserved before the file lands, and the ratio does the rest.

   Set from the wordmark, not the file. The lockup is 4.5:1 with the type only
   about 45% of the artwork's height, so matching the old 20px brand text takes
   a 2.5rem box - which is still short of the 4rem capsule it sits in. */
.brand__logo {
  display: block;
  height: 2.5rem;
  width: auto;
}

/* One nav element, two layouts. Above 60rem it is the row of pills inside the
   capsule; below it the same list is fixed to the bottom of the screen as the
   tab bar (see the responsive block at the foot of this file). The icons ship
   in the markup and are hidden on desktop, so the mobile nav needs no script
   and no second copy of the links. */
.nav {
  display: flex;
  align-items: center;
  gap: var(--os-space-1);
  margin-inline-start: var(--os-space-3);
}

.nav__link {
  display: inline-flex;
  align-items: center;
  padding: var(--os-space-2) var(--os-space-3);
  border-radius: var(--os-radius-sm);
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-medium);
  color: var(--color-text-muted);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--os-duration-fast) var(--os-ease-out),
    color var(--os-duration-fast) var(--os-ease-out);
}

/* Touch has no hover, so every hover affordance below is paired with an
   :active state that fires on tap. */
.nav__link:hover,
.nav__link:active {
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

/* Active page: a filled chip of the site's own ink, per the direction. */
.nav__link[aria-current="page"] {
  background: var(--color-text);
  color: var(--color-surface);
}

.nav__link[aria-current="page"]:hover,
.nav__link[aria-current="page"]:active {
  background: var(--color-text);
  color: var(--color-surface);
}

/* The icon is the mobile tab bar's; on desktop the label carries the link. */
.nav__icon { display: none; }

/* Search is a tab on the phone and the command entry on the desktop, so each
   control is hidden where the other one is the real search. */
.nav__link--search { display: none; }

/* The command entry. Not an input: it opens the palette, which is where the
   typing happens, so it can say what the palette searches and show the key
   that opens it. */
.cmd {
  margin-inline-start: auto;
  flex: 0 1 20rem;
  min-width: 11rem;
  display: flex;
  align-items: center;
  gap: var(--os-space-2);
  height: 2.5rem;
  padding-inline: var(--os-space-3) var(--os-space-2);
  background: var(--color-surface-sunken);
  border: var(--os-border-hair) solid transparent;
  border-radius: var(--os-radius-md);
  color: var(--color-text-subtle);
  font-family: inherit;
  font-size: var(--os-text-sm);
  text-align: start;
  cursor: pointer;
  transition: border-color var(--os-duration-fast) var(--os-ease-out),
    color var(--os-duration-fast) var(--os-ease-out);
}

.cmd:hover,
.cmd:active {
  border-color: var(--color-border-strong);
  color: var(--color-text-muted);
}

.cmd__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The keycap. Present on the desktop capsule only, because it is a promise
   about a keyboard the reader has. */
.cmd__key {
  flex-shrink: 0;
  display: inline-grid;
  place-items: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding-inline: var(--os-space-1);
  border: var(--os-border-hair) solid var(--color-border-strong);
  border-radius: var(--os-radius-sm);
  background: var(--color-surface-raised);
  font-family: inherit;
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  color: var(--color-text-muted);
}

/* ----------------------------------------- 4b. The search palette -------- */

/* The other half of the command surface. One dialog, two shapes: a panel
   hanging under the capsule on a desktop, a sheet rising from the thumb on a
   phone. ui.js injects the markup once per page and fills the list from
   books.json the first time it is opened. */

.palette {
  position: fixed;
  inset: 0;
  z-index: var(--os-z-overlay);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  /* The gutter is the capsule's, so the panel hangs on the capsule's own two
     edges rather than floating loose inside it. */
  padding: 0 var(--os-gutter) var(--os-space-4);
}

.palette[hidden] { display: none; }

.palette__scrim {
  position: absolute;
  inset: 0;
  background: var(--color-scrim);
}

.palette__panel {
  position: relative;
  width: 100%;
  max-width: calc(var(--os-container-md) - (2 * var(--os-gutter)));
  /* Clear the capsule it belongs to: the panel reads as the capsule's own
     drawer rather than a dialog that landed on top of it. */
  margin-block-start: calc(var(--header-h, var(--os-header-height)) + var(--os-space-2));
  max-height: calc(100dvh - var(--header-h, var(--os-header-height)) - var(--os-space-8));
  display: flex;
  flex-direction: column;
  background: var(--color-surface-raised);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
  box-shadow: var(--os-shadow-lg);
  overflow: hidden;
}

.palette__field {
  display: flex;
  align-items: center;
  gap: var(--os-space-3);
  padding: var(--os-space-3) var(--os-space-4);
  border-block-end: var(--os-border-hair) solid var(--color-divider);
  color: var(--color-text-subtle);
}

.palette__input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: var(--os-text-md);
  color: var(--color-text);
}

.palette__input:focus { outline: none; }

/* The field is where focus lands the moment the palette opens, so a full ring
   would fire on every open. The rule under it takes the accent instead - the
   same coral underline the site marks an active thing with - and the input
   keeps no outline of its own. */
.palette__field:focus-within {
  border-block-end-color: var(--color-accent);
  box-shadow: inset 0 -1px 0 var(--color-accent);
}

.palette__input::placeholder { color: var(--color-text-subtle); }

/* Safari draws its own clear button inside a search input; the palette has an
   Escape key and a close control and does not need a third. */
.palette__input::-webkit-search-cancel-button { display: none; }

.palette__close {
  flex-shrink: 0;
  padding: var(--os-space-1) var(--os-space-2);
  border: var(--os-border-hair) solid var(--color-border-strong);
  border-radius: var(--os-radius-sm);
  background: var(--color-surface-raised);
  font-family: inherit;
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  color: var(--color-text-muted);
  cursor: pointer;
}

.palette__close:hover,
.palette__close:active {
  border-color: var(--color-accent-border);
  color: var(--color-accent);
}

.palette__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--os-space-3) var(--os-space-2) var(--os-space-2);
}

.palette__head {
  margin: 0;
  padding: var(--os-space-1) var(--os-space-3) var(--os-space-2);
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-bold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--color-text-subtle);
}

.palette__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.palette__option {
  display: flex;
  align-items: center;
  gap: var(--os-space-3);
  padding: var(--os-space-2) var(--os-space-3);
  border-radius: var(--os-radius-sm);
  color: var(--color-text);
  text-decoration: none;
  cursor: pointer;
}

/* One highlight, driven by the keyboard and by the pointer alike, so arrowing
   down and moving the mouse never leave two rows looking selected. */
.palette__option[aria-selected="true"] {
  background: var(--color-surface-sunken);
}

.palette__option-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-medium);
}

.palette__option-meta {
  flex-shrink: 0;
  font-size: var(--os-text-xs);
  color: var(--color-text-subtle);
}

.palette__option-icon {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  color: var(--color-text-subtle);
}

.palette__empty {
  padding: var(--os-space-4) var(--os-space-3);
  margin: 0;
  font-size: var(--os-text-sm);
  color: var(--color-text-muted);
}

.palette__foot {
  margin: 0;
  padding: var(--os-space-3);
  border-block-start: var(--os-border-hair) solid var(--color-divider);
  font-size: var(--os-text-xs);
  color: var(--color-text-subtle);
}

/* Navy inverse block, per the brand's footer treatment. */
.site-footer {
  flex-shrink: 0;
  background: var(--color-surface-inverse);
  padding-block: var(--os-space-6);
  position: relative;
  z-index: 1;
}

.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--os-space-4);
  font-size: var(--os-text-sm);
  color: var(--color-footer-text);
}

.site-footer__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--os-space-3);
}

/* The footer sits on the inverse surface, so its mark takes the light-ink
   artwork rather than the navy one the header's lockup uses. */
.site-footer__mark {
  display: inline-block;
  height: 1.6rem;
  aspect-ratio: 924 / 660;
  flex-shrink: 0;
  background: url("../assets/open-shelf-mark-dark.png") center / contain
    no-repeat;
}

.site-footer__links {
  display: flex;
  gap: var(--os-space-4);
  flex-wrap: wrap;
}

/* ---------------------------------------- 5. Buttons and control atoms --- */

/* 8px radius, 1.5px borders, Inter 600. No press-state transform - the
   brand's press feedback is colour only. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--os-space-2);
  padding: var(--os-space-3) var(--os-space-5);
  border: var(--os-border-mid) solid transparent;
  border-radius: var(--os-radius-sm);
  font-size: var(--os-text-md);
  font-weight: var(--os-weight-semibold);
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--os-duration-fast) var(--os-ease-out),
    border-color var(--os-duration-fast) var(--os-ease-out),
    color var(--os-duration-fast) var(--os-ease-out);
}

.btn:active { background-color: var(--color-accent-active); }

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn--primary {
  background: var(--color-accent);
  color: var(--color-text-on-accent);
}

.btn--primary:hover {
  background: var(--color-accent-hover);
  color: var(--color-text-on-accent);
}

.btn--secondary {
  background: var(--color-surface-raised);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.btn--secondary:hover {
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

.btn--secondary:active { background-color: var(--color-border); }

.btn--ghost {
  background: transparent;
  color: var(--color-accent);
}

.btn--ghost:hover {
  background: var(--color-surface-sunken);
  color: var(--color-accent);
}

.btn--ghost:active { background-color: var(--color-border); }

/* The outline button used on the navy bands of the book detail page. It reads
   against the inverse surface in both themes, so it takes the brand's fixed
   navy palette rather than the theme-flipping semantic tokens. */
.btn--on-dark {
  background: transparent;
  border-color: var(--os-navy-600);
  color: var(--os-sand-100);
}

.btn--on-dark:hover {
  border-color: var(--os-sand-100);
  background: rgba(251, 245, 238, 0.08);
  color: var(--os-sand-100);
}

.btn--on-dark:active { background-color: rgba(251, 245, 238, 0.14); }

.btn--sm {
  padding: var(--os-space-2) var(--os-space-4);
  font-size: var(--os-text-sm);
}

.btn--lg {
  padding: var(--os-space-4) var(--os-space-6);
  font-size: var(--os-text-lg);
}

.btn--block {
  width: 100%;
}

.link-back {
  display: inline-flex;
  align-items: center;
  gap: var(--os-space-2);
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-medium);
  color: var(--color-text-muted);
  text-decoration: none;
  margin-block-end: var(--os-space-5);
}

.link-back:hover,
.link-back:active { color: var(--color-accent); }

/* ------------------------------------------------------ 6. Form fields --- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-2);
}

.field__label {
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-subtle);
}

.input,
.select {
  width: 100%;
  padding: var(--os-space-3) var(--os-space-4);
  background: var(--color-surface-raised);
  border: var(--os-border-mid) solid var(--color-border-strong);
  border-radius: var(--os-radius-sm);
  font-size: var(--os-text-md);
  color: var(--color-text);
  transition: border-color var(--os-duration-fast) var(--os-ease-out),
    box-shadow var(--os-duration-fast) var(--os-ease-out);
}

.input::placeholder { color: var(--color-text-subtle); }

.input:focus,
.select:focus {
  outline: none;
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

.select {
  appearance: none;
  padding-inline-end: var(--os-space-7);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 1.15rem) 55%, calc(100% - 0.8rem) 55%;
  background-size: 0.35rem 0.35rem, 0.35rem 0.35rem;
  background-repeat: no-repeat;
  cursor: pointer;
}

/* Positioning context for the icon only. It must not grow: inside a column
   flex container a flexible height would stretch the wrapper and strand the
   absolutely positioned icon halfway down the rail. */
.search {
  position: relative;
  flex: 0 0 auto;
}

.search--grow {
  flex: 1 1 18rem;
}

.search .input {
  padding-inline-start: var(--os-space-7);
}

.search__icon {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: var(--os-space-4);
  transform: translateY(-50%);
  color: var(--color-text-subtle);
  pointer-events: none;
  line-height: 0;
}

/* ------------------------------------------- 7. Chips, pills and tags ---- */

/* Brand chip: tinted background with same-hue 600-weight text, no border.
   The transparent border keeps the box the same size as the pressed state. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--os-space-2);
  padding: var(--os-space-2) var(--os-space-4);
  border: var(--os-border-hair) solid transparent;
  border-radius: var(--os-radius-pill);
  background: var(--color-surface-sunken);
  color: var(--color-text-muted);
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-semibold);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--os-duration-fast) var(--os-ease-out),
    border-color var(--os-duration-fast) var(--os-ease-out),
    color var(--os-duration-fast) var(--os-ease-out);
}

.chip:hover,
.chip:not(:disabled):active {
  background: var(--color-border);
  color: var(--color-text);
}

.chip[aria-pressed="true"] {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
}

.chip__count {
  font-size: var(--os-text-2xs);
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--os-space-2);
}

.chip-row--scroll {
  flex-wrap: nowrap;
  overflow-x: auto;
  padding-block-end: var(--os-space-2);
  scrollbar-width: thin;
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--os-space-1);
  max-width: 100%;
  padding: var(--os-space-1) var(--os-space-3);
  border-radius: var(--os-radius-pill);
  background: var(--color-support-soft);
  border: var(--os-border-hair) solid var(--color-support-border);
  color: var(--color-support);
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

.pill--accent {
  background: var(--color-accent-soft);
  border-color: var(--color-accent-border);
  color: var(--color-accent);
}

/* Scraped shelf names arrive as long hyphenated slugs, so this variant is the
   one pill allowed to wrap rather than run off a 360px screen. */
.pill--plain {
  background: var(--color-surface-sunken);
  border-color: var(--color-border);
  color: var(--color-text-muted);
  white-space: normal;
  overflow-wrap: anywhere;
  text-transform: none;
  letter-spacing: var(--os-tracking-normal);
  font-weight: var(--os-weight-medium);
  font-size: var(--os-text-xs);
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--os-space-2);
}

/* ------------------------------------------------------- 8. Book covers --
   Most books ship a real jacket, served from the Azure covers container as
   covers/<id>. The rest fall back to a cover composed from the book's
   own title and author over a token-defined gradient; js/ui.js picks the
   palette index from a hash of the book id, so a book keeps the same spine
   everywhere it appears.

   The two are layered, not swapped: .cover always carries the generated art
   and .cover__img sits over it. A jacket that fails to load therefore leaves
   something readable behind rather than an empty rectangle. */

.cover {
  position: relative;
  aspect-ratio: 2 / 3;
  border-radius: var(--os-radius-xs) var(--os-radius-sm) var(--os-radius-sm)
    var(--os-radius-xs);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--os-space-4) var(--os-space-3) var(--os-space-4) var(--os-space-4);
  background-image: linear-gradient(150deg, var(--cover-from), var(--cover-to));
  color: var(--cover-ink);
  box-shadow: var(--os-shadow-book);
  isolation: isolate;
}

.cover--1 { --cover-from: var(--cover-1-from); --cover-to: var(--cover-1-to); --cover-ink: var(--cover-1-ink); }
.cover--2 { --cover-from: var(--cover-2-from); --cover-to: var(--cover-2-to); --cover-ink: var(--cover-2-ink); }
.cover--3 { --cover-from: var(--cover-3-from); --cover-to: var(--cover-3-to); --cover-ink: var(--cover-3-ink); }
.cover--4 { --cover-from: var(--cover-4-from); --cover-to: var(--cover-4-to); --cover-ink: var(--cover-4-ink); }
.cover--5 { --cover-from: var(--cover-5-from); --cover-to: var(--cover-5-to); --cover-ink: var(--cover-5-ink); }
.cover--6 { --cover-from: var(--cover-6-from); --cover-to: var(--cover-6-to); --cover-ink: var(--cover-6-ink); }

/* Spine highlight down the binding edge. */
.cover::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  width: 0.5rem;
  background: linear-gradient(
    90deg,
    rgba(0, 0, 0, 0.28),
    rgba(255, 255, 255, 0.12) 60%,
    transparent
  );
  z-index: 1;
}

.cover__title {
  font-family: var(--os-font-display);
  font-size: var(--os-text-md);
  font-weight: var(--os-weight-bold);
  line-height: var(--os-leading-snug);
  letter-spacing: var(--os-tracking-tight);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 5;
  line-clamp: 5;
  overflow: hidden;
}

.cover__author {
  font-size: var(--os-text-2xs);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  opacity: 0.85;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

.cover__rule {
  width: 2rem;
  height: var(--os-border-thick);
  background: currentColor;
  opacity: 0.55;
  margin-block-end: var(--os-space-2);
}

.cover--lg {
  padding: var(--os-space-5);
}

.cover--lg .cover__title { font-size: var(--os-text-xl); }
.cover--lg .cover__author { font-size: var(--os-text-xs); }

.cover--sm {
  padding: var(--os-space-2);
  border-radius: var(--os-radius-xs);
}

.cover--sm .cover__title {
  font-size: var(--os-text-2xs);
  -webkit-line-clamp: 4;
  line-clamp: 4;
}

.cover--sm .cover__author { display: none; }
.cover--sm .cover__rule { display: none; }

/* The scraped jackets sit within a few percent of 2:3 either way, so they are
   cropped to the tile rather than letterboxed - an even grid is worth more
   than the last few pixels of a jacket border. */
.cover__img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  z-index: 1;
}

/* Keep the binding highlight over the artwork, so a real jacket still reads
   as a book rather than a poster. */
.cover--art::before { z-index: 2; }

/* ------------------------------------------- 9. Book cards and grids ----- */

.book-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
  gap: var(--os-space-6) var(--os-space-5);
}

/* The catalogue grid sits beside the filter rail, so its minimum has to be
   small enough that four columns still fit the narrower track. */
.book-grid--wide {
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
}

/* Hover lifts the shadow only - the brand's motion is colour and shadow,
   never movement. */
.book-card {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-3);
  text-decoration: none;
  color: inherit;
  border-radius: var(--os-radius-md);
}

.book-card:hover {
  color: inherit;
}

.book-card:hover .cover {
  box-shadow: var(--os-shadow-lg);
}

.book-card:active .cover {
  box-shadow: var(--os-shadow-md);
}

.book-card .cover {
  transition: box-shadow var(--os-duration-base) var(--os-ease-out);
}

.book-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-1);
}

.book-card__title {
  font-family: var(--os-font-display);
  font-size: var(--os-text-md);
  font-weight: var(--os-weight-semibold);
  line-height: var(--os-leading-snug);
  overflow-wrap: anywhere;
  /* Reserve both clamped lines so ratings align across a row of cards
     regardless of how long each title runs. */
  min-block-size: calc(var(--os-text-md) * var(--os-leading-snug) * 2);
}

.book-card:hover .book-card__title,
.book-card:active .book-card__title {
  color: var(--color-accent);
}

.book-card__author {
  font-size: var(--os-text-sm);
  color: var(--color-text-muted);
  overflow-wrap: anywhere;
}

.book-card__foot {
  display: flex;
  align-items: center;
  gap: var(--os-space-3);
  margin-block-start: var(--os-space-1);
}

/* Stars, score and count together are within a few pixels of a two-column
   card at 360px. The tighter gap keeps the usual book on one line and leaves
   the wrap in .rating as the fallback for the widest counts. */
.book-card__foot .rating { gap: var(--os-space-1); }

/* ------------------------------------------------- 10. Rating display ---- */

/* Wraps rather than overflows: five stars plus the score plus a ratings count
   run past a two-column card at 360px, and how far past depends on the book
   (a "(1.2M)" is wider than a "(985)"). The stars stay on one line either way,
   so the fallback is the count dropping below them. */
.rating {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--os-space-2);
  max-width: 100%;
  font-size: var(--os-text-sm);
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

.stars {
  display: inline-flex;
  gap: 1px;
  color: var(--color-star);
  line-height: 1;
}

.stars__star--empty { color: var(--color-star-empty); }

.rating__value {
  font-weight: var(--os-weight-semibold);
  color: var(--color-text);
}

/* ----------------------------------------------------- 11. Home page ----- */

/* Sand-tint hero panel - the warm layer of the brand's surface stack. */
/* The pitch and the catalogue figures are one card: the numbers are evidence
   for the claim above them, so a shared surface with a hairline divider reads
   better than two cards with a seam of canvas between them. */
.hero-panel {
  background: var(--color-surface);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
  box-shadow: var(--os-shadow-sm);
  overflow: hidden;
}

.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  align-items: center;
  gap: var(--os-space-8);
  padding: var(--os-space-8) var(--os-space-7);
}

/* Flush into the panel: the divider does the work the card chrome did. */
.hero-panel .stat-row {
  background: none;
  border: 0;
  border-block-start: var(--os-border-hair) solid var(--color-border);
  border-radius: 0;
  box-shadow: none;
  padding: var(--os-space-6) var(--os-space-7);
}

.hero__title {
  font-size: var(--os-text-4xl);
  margin-block: var(--os-space-4) var(--os-space-5);
  max-width: 14ch;
}

/* Hold the pitch to a readable measure instead of letting it run the full
   column at wide viewports. */
.hero .lede {
  max-width: 46ch;
}

.hero__title em {
  font-style: italic;
  color: var(--color-accent);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--os-space-3);
  margin-block-start: var(--os-space-6);
}

/* A fanned stack of generated covers, purely decorative. Hidden on phones so
   the pitch and its CTA fit the first screen; the covers are lazy-loaded, so a
   hidden fan also never spends the three requests. */
.hero__shelf {
  display: none;
  justify-content: center;
  align-items: center;
  perspective: 1200px;
}

@media (min-width: 40.0625rem) {
  .hero__shelf { display: flex; }
}

/* The covers overlap into a fan rather than sitting in three separate cells:
   the middle one comes forward, the outer two tuck behind and splay. */
.hero__shelf .book-card {
  flex: 0 0 auto;
  width: min(11rem, 33%);
  transition: transform var(--os-duration-base) var(--os-ease-out);
}

.hero__shelf .book-card:nth-child(1) {
  z-index: 1;
  transform: rotate(-7deg) translateY(var(--os-space-3));
}

.hero__shelf .book-card:nth-child(2) {
  z-index: 3;
  margin-inline: calc(var(--os-space-4) * -1);
  transform: scale(1.08) translateY(calc(var(--os-space-2) * -1));
}

.hero__shelf .book-card:nth-child(3) {
  z-index: 2;
  transform: rotate(7deg) translateY(var(--os-space-3));
}

/* Lift the hovered cover clear of its neighbours. */
.hero__shelf .book-card:nth-child(1):hover {
  z-index: 4;
  transform: rotate(-7deg) translateY(0);
}

.hero__shelf .book-card:nth-child(2):hover {
  transform: scale(1.08) translateY(calc(var(--os-space-4) * -1));
}

.hero__shelf .book-card:nth-child(3):hover {
  z-index: 4;
  transform: rotate(7deg) translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .hero__shelf .book-card { transition: none; }
}

.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--os-space-4);
  padding: var(--os-space-6);
  background: var(--color-surface);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
  box-shadow: var(--os-shadow-sm);
}

/* Navy figures under teal tracked labels; coral stays reserved for actions. */
.stat__value {
  font-family: var(--os-font-display);
  font-size: var(--os-text-2xl);
  font-weight: var(--os-weight-bold);
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
  line-height: var(--os-leading-tight);
}

.stat__label {
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-wide);
  text-transform: uppercase;
  color: var(--color-support);
  margin-block-start: var(--os-space-1);
}

.feature-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--os-space-5);
}

.feature-card {
  padding: var(--os-space-6);
  background: var(--color-surface);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
  display: flex;
  flex-direction: column;
  gap: var(--os-space-3);
}

/* Teal icon wells - teal structures, coral acts. */
.feature-card__icon {
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  border-radius: var(--os-radius-sm);
  background: var(--color-support-soft);
  color: var(--color-support);
  font-size: var(--os-text-lg);
  line-height: 0;
}

.feature-card__title {
  font-size: var(--os-text-lg);
}

.mood-picker {
  padding: var(--os-space-6);
  border: var(--os-border-thick) dashed var(--color-border-strong);
  border-radius: var(--os-radius-lg);
  background: var(--color-surface);
}

/* ------------------------------------------------- 11.1 the engine -------
   The one interactive thing on the page, built on the two directions the
   project committed to.

   From the book page's seam: no card anywhere in this section. A pair of
   panels reads as a comparison table, and what the engine returns is an
   argument - so each result is argued across one hairline that runs teal at
   the top to coral at the bottom, and the bench is ruled rather than boxed.

   From the wall: the jacket carries the book's own reading. The claim and the
   split behind it ride into the result row beside the cover, the same two
   numbers the wall paints on its scrim.

   Coral acts, teal structures. The picks are structure, so a filled slot goes
   teal; running the engine is the act, so the button is the only coral thing
   above the results. */

.engine__title { max-width: 22ch; }

.engine__lede { margin-block-start: var(--os-space-4); }

/* --- the bench --- */

/* One rule holds this section together: white is where you act, the page's
   own surface is where you read. The bench is therefore a white band run to
   the container's edges - a band, not a card, so no radius and no shadow -
   and the slate below it stays on the surface, unboxed. */
.bench {
  margin-block-start: var(--os-space-6);
  margin-inline: calc(var(--os-gutter) * -1);
  padding: var(--os-space-5) var(--os-gutter) var(--os-space-6);
  background: var(--color-surface-raised);
  border-block: var(--os-border-hair) solid var(--color-border);
}

/* The one sentence that has to be read before anything is clicked, so it is
   set in ink at body size rather than as subtle grey chrome. */
.bench__ask {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--os-space-1) var(--os-space-4);
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-normal);
  color: var(--color-text);
}

.bench__ask > span { flex: 1 1 22rem; }

.bench__label {
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--color-support);
}

.bench__cue { font-weight: var(--os-weight-semibold); }

/* Three across wherever three fit, one per row on a phone. Flowed rather than
   switched at the section's own breakpoint: a full-width slot leaves its Swap
   stranded half a screen from the title it belongs to. */
.bench__slots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--os-space-3);
  margin-block-start: var(--os-space-4);
  min-block-size: 3.75rem;
}

/* A slot at rest: a bordered control, because a picked book set like prose
   does not read as pressable and swapping is the only thing to do here. The
   jacket keeps a column of its own at a fixed width and the name column is
   free to shrink, so a long title crops rather than pushing Swap off the end
   or riding over the cover. */
.eslot {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--os-space-3);
  inline-size: 100%;
  padding: var(--os-space-3);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-sm);
  background: var(--color-surface-raised);
  font: inherit;
  color: inherit;
  text-align: start;
  cursor: pointer;
  transition: border-color var(--os-duration-fast) var(--os-ease-out);
}

.eslot:hover { border-color: var(--color-border-strong); }

.eslot__jacket {
  display: block;
  inline-size: 2.25rem;
}

/* At 36px the generated title has about half a word of room, so it clips into
   nonsense. The bench slot names the book beside the jacket anyway - the
   jacket here is only the object. */
.eslot__jacket .cover__title { display: none; }

.eslot__name {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
}

.eslot__title {
  font-family: var(--os-font-display);
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-semibold);
  line-height: var(--os-leading-snug);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.eslot__byline {
  font-size: var(--os-text-2xs);
  color: var(--color-text-subtle);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The section's one coral: there is no submit button to spend it on. */
.eslot__swap {
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  color: var(--color-text-muted);
  border-block-end: var(--os-border-hair) solid var(--color-accent);
  padding-block-end: 1px;
  white-space: nowrap;
}

.eslot:hover .eslot__swap { color: var(--color-text); }

/* The slot open. A teal ring rather than a coral one: choosing is structure,
   and the coral was spent on the swap that got you here. */
.eslot--open {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--os-space-2);
  cursor: default;
  border-color: var(--color-support);
  box-shadow: 0 0 0 3px var(--color-support-soft);
}

.eslot__label {
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--color-support);
}

/* A line to write on rather than a fielded box: the ring around the slot is
   already saying the field is live. */
.eslot__input {
  inline-size: 100%;
  padding: 0 0 var(--os-space-2);
  border: 0;
  border-block-end: var(--os-border-hair) solid var(--color-border-strong);
  border-radius: 0;
  background: none;
  font: inherit;
  font-size: var(--os-text-sm);
  color: var(--color-text);
}

.eslot__input::placeholder { color: var(--color-text-subtle); }

.eslot__input:focus {
  outline: none;
  border-block-end-color: var(--color-accent);
}

.eslot__cancel {
  justify-self: start;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--os-text-2xs);
  color: var(--color-text-muted);
  cursor: pointer;
}

.eslot__cancel:hover { color: var(--color-text); }

@media (prefers-reduced-motion: reduce) {
  .eslot { transition: none; }
}

/* The five have a stated relationship to the three above them rather than
   just following on after a gap. */
.slate-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--os-space-2) var(--os-space-4);
  margin-block-start: var(--os-space-7);
  padding-block-end: var(--os-space-3);
  border-block-end: var(--os-border-hair) solid var(--color-border);
}

.slate-head b {
  font-family: var(--os-font-display);
  font-size: var(--os-text-lg);
  font-weight: var(--os-weight-bold);
  letter-spacing: var(--os-tracking-tight);
}

.slate-head span {
  font-size: var(--os-text-xs);
  color: var(--color-text-subtle);
}

/* A re-run keeps the old argument on screen and dims it, rather than throwing
   the reader back to a skeleton they have already read past. */
[data-recommendation-results][aria-busy="true"] .slate {
  opacity: 0.45;
  transition: opacity var(--os-duration-fast) var(--os-ease-out);
}

/* --- the slate --- */

/* Five arguments down one column, separated by hairlines. No grid of cards:
   a recommendation is a sentence about a pair of books, and a card would make
   it look like stock. */
.slate {
  list-style: none;
  margin: var(--os-space-4) 0 0;
  padding: 0;
}

.slate__row {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: var(--os-space-5);
  padding-block: var(--os-space-6);
  border-block-start: var(--os-border-hair) solid var(--color-border);
}

.slate__row:first-child { border-block-start: 0; padding-block-start: var(--os-space-2); }

.slate__jacket {
  display: block;
  align-self: start;
  border-radius: 2px;
  overflow: hidden;
}

.slate__body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--os-space-5);
}

/* What the book is, and what readers say about it - before the engine says
   anything about the match. */
.slate__id {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 14rem);
  gap: var(--os-space-5);
  align-items: start;
}

.slate__name {
  position: relative;
  margin: 0;
  padding-inline-start: 2.5rem;
  display: flex;
  flex-direction: column;
  gap: var(--os-space-1);
}

/* The rank, set as a numeral in the display face and left in subtle ink: it
   orders the five without competing with the titles. */
.slate__rank {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: -0.05em;
  font-family: var(--os-font-display);
  font-size: var(--os-text-2xl);
  font-weight: var(--os-weight-bold);
  line-height: 1;
  color: var(--color-text-subtle);
}

.slate__link {
  font-family: var(--os-font-display);
  font-size: var(--os-text-lg);
  font-weight: var(--os-weight-bold);
  letter-spacing: var(--os-tracking-tight);
  line-height: var(--os-leading-snug);
  color: var(--color-text);
  text-decoration: none;
}

.slate__link:hover { text-decoration: underline; }

.slate__byline {
  font-size: var(--os-text-xs);
  color: var(--color-text-subtle);
}

/* The wall's payload, travelling with the cover. Right-aligned against the
   row's outer edge so it reads as the book's own margin note rather than as
   the first line of the argument below it. */
.slate__claim {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-1);
  text-align: end;
}

.slate__claim-line {
  margin: 0;
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  line-height: var(--os-leading-snug);
  color: var(--color-support);
}

.slate__evidence {
  font-size: var(--os-text-2xs);
  color: var(--color-text-subtle);
}

.slate__thin {
  margin: 0;
  font-size: var(--os-text-2xs);
  line-height: var(--os-leading-normal);
  color: var(--color-text-subtle);
  text-align: end;
}

.slate .split { height: 0.25rem; }

/* --- the two readings --- */

/* The book page's seam, at row scale. What the pair share hangs off the teal
   end, what the book asks instead off the coral end, and neither side is
   tinted as the right answer because they are named by the ends of one rule
   they share. */
.readings {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
  column-gap: var(--os-space-6);
}

/* The engine reached it but has nothing to put on the other side, or never
   reached it at all. One reading, no seam, capped rather than full-bleed -
   the same rule .cases--single follows on the book page. */
.readings--single {
  grid-template-columns: minmax(0, 1fr);
  max-width: 34rem;
}

.readings__seam {
  background: linear-gradient(
    var(--color-support),
    var(--color-divider) 40%,
    var(--color-divider) 60%,
    var(--color-accent)
  );
}

.readings__seam span { display: none; }

.reading {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-2);
}

/* The right-hand reading presses on the seam from its own side. */
.reading--asks { text-align: end; }

.reading__label {
  display: flex;
  align-items: baseline;
  gap: var(--os-space-2);
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--os-tracking-caps);
}

.reading--shares .reading__label { color: var(--color-support); }

.reading--asks .reading__label {
  color: var(--color-accent);
  flex-direction: row-reverse;
}

.reading__label b {
  font-family: var(--os-font-display);
  font-size: var(--os-text-md);
  font-weight: var(--os-weight-bold);
  letter-spacing: var(--os-tracking-tight);
  line-height: 1;
  text-transform: none;
}

/* Each sentence hangs off its own side on a short rule of that side's colour,
   so it is placed by where it sits and not by a second label. */
.reading__line {
  margin: 0;
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-relaxed);
  color: var(--color-text);
}

.reading__evidence {
  margin: 0;
  font-size: var(--os-text-2xs);
  line-height: var(--os-leading-normal);
  color: var(--color-text-subtle);
}

.reading--shares .reading__line,
.reading--shares .reading__evidence {
  border-inline-start: 2px solid var(--color-support-border);
  padding-inline-start: var(--os-space-4);
}

.reading--shares .reading__evidence { border-inline-start-color: transparent; }

.reading--asks .reading__line {
  border-inline-end: 2px solid var(--color-accent-border);
  padding-inline-end: var(--os-space-4);
}

/* The bench folds to one slot per row, and the seam folds the way the book
   page's does: both readings run down one rail that changes colour at the
   turn, and the seam becomes the divider that names it. */
@media (max-width: 60rem) {
  .slate__id { grid-template-columns: minmax(0, 1fr); }

  .slate__claim,
  .slate__thin { text-align: start; }

  .readings {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--os-space-4);
  }

  .reading--asks { text-align: start; }

  .reading--asks .reading__label { flex-direction: row; }

  .reading--shares .reading__line,
  .reading--shares .reading__evidence,
  .reading--asks .reading__line {
    border: 0;
    padding: 0;
  }

  .reading {
    border-inline-start: 2px solid var(--color-support-border);
    padding-inline-start: var(--os-space-4);
  }

  .reading--asks { border-inline-start-color: var(--color-accent-border); }

  .readings__seam {
    display: flex;
    align-items: center;
    gap: var(--os-space-3);
    background: none;
  }

  .readings__seam::before,
  .readings__seam::after {
    content: "";
    flex: 1;
    border-block-start: var(--os-border-hair) solid var(--color-divider);
  }

  .readings__seam span {
    display: inline;
    font-size: var(--os-text-2xs);
    font-weight: var(--os-weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--os-tracking-caps);
    color: var(--color-text-subtle);
  }
}

@media (max-width: 40rem) {
  .slate__row {
    grid-template-columns: 3.25rem minmax(0, 1fr);
    gap: var(--os-space-4);
  }

  .slate__name { padding-inline-start: 2rem; }

  .slate__rank { font-size: var(--os-text-xl); }
}

/* ------------------------------------------------ 12. Book list page -----
   Design direction 2b, "the wall", labelling itself. The page runs edge to
   edge: a mood bar across the top, and forty-five jackets packed against each
   other underneath it at the full width. books.html therefore carries
   .page--bleed and no container of its own.

   A jacket carries its own reading rather than filling in a panel beside the
   wall: a navy scrim on hover or focus where there is a pointer, a caption
   under the cover where there is not. The scrim is a navy band, so everything
   painted on it reads from the theme-invariant --os-ink-* ramp in tokens.css,
   the same convention the book detail page follows. */

/* --- 12.1 the mood bar --- */

/* Sticks under the floating capsule, so it follows the header's measured
   height - ui.js publishes --header-h - rather than the designed token. The
   capsule's shadow and its own padding put the real header well above 4rem. */
.mood-bar {
  position: sticky;
  inset-block-start: var(--header-h, var(--os-header-height));
  z-index: var(--os-z-sticky);
  background: var(--color-surface);
  border-block-end: var(--os-border-hair) solid var(--color-border);
}

/* The bar's rule runs edge to edge; what sits on it lines up with the wall
   below, so the chips and the count share the wall's left and right edges. */
.mood-bar__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--os-space-3) var(--os-space-4);
  max-width: var(--os-container-lg);
  margin-inline: auto;
  padding: var(--os-space-3) var(--os-gutter);
}

.mood-bar__spacer {
  flex: 1 1 auto;
}

.mood-bar__count {
  font-size: var(--os-text-sm);
  color: var(--color-text-subtle);
}

.mood-bar__count strong {
  color: var(--color-text);
  font-weight: var(--os-weight-semibold);
}

/* --- 12.2 the layout --- */

/* The wall takes the whole column. There is no second one: the shelf talker
   held a fixed 22rem of every screen and stood empty until a cover was
   clicked, and everything it said now rides on the jacket itself.

   The page is full-bleed, but the wall is not: it stops at the same measure
   as .container, so the covers line up with every other page on the site and
   a cover on a wide monitor stays a cover rather than a stamp. The bands
   either side of it - the mood bar's rule, the sticky background - still run
   edge to edge, which is what .page--bleed is for. */
.wall-main {
  min-width: 0;
  width: 100%;
  max-width: var(--os-container-lg);
  margin-inline: auto;
  padding: var(--os-space-5) var(--os-gutter) var(--os-space-9);
}

/* --- 12.3 the jacket's own reading --- */

/* Everything the shelf talker used to say, on the jacket it says it about.
   One block, two presentations: a caption under the cover by default, and a
   navy scrim over it wherever there is a pointer and room for one. The switch
   is made here and nowhere else - book-list.js emits the same markup for
   both. */
.tile__meta {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-1);
  min-width: 0;
}

.tile__title {
  font-family: var(--os-font-display);
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  line-height: var(--os-leading-snug);
  color: var(--color-text);
  /* Two lines of title, then the author. A third line pushes the claim off
     the bottom of a scrim and out of the row on a caption. Always two, even
     for a one-word title: as a caption the block sets from the top, and the
     claims across a row line up only if the titles above them do. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  min-height: calc(2em * var(--os-leading-snug));
}

.tile__byline {
  font-size: var(--os-text-2xs);
  color: var(--color-text-subtle);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The claim is a sentence readers made, not a category, so it is set as one:
   teal and emphasised, but never shouted in caps - most claims run five or six
   words and an uppercase phrase that long stops being a label. */
.tile__claim {
  margin: var(--os-space-1) 0 0;
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  line-height: var(--os-leading-snug);
  color: var(--color-support);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.tile__evidence {
  font-size: var(--os-text-2xs);
  color: var(--color-text-subtle);
}

/* A book the corpus never earned a claim for. Said plainly rather than filled
   in - see the honesty rules in docs/03-data-sources-as-built.md. */
.tile__thin {
  margin: var(--os-space-1) 0 0;
  font-size: var(--os-text-2xs);
  line-height: var(--os-leading-normal);
  color: var(--color-text-subtle);
}

.tile .split {
  height: 0.25rem;
  margin-block-start: var(--os-space-1);
}

.tile__shelf {
  align-self: flex-start;
  margin-block-start: var(--os-space-2);
  padding: var(--os-space-1) var(--os-space-3);
  border: var(--os-border-hair) solid var(--color-border-strong);
  border-radius: var(--os-radius-sm);
  background: none;
  color: var(--color-text);
  font: inherit;
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  cursor: pointer;
}

.tile__shelf:hover { border-color: var(--color-text); }

.tile__shelf[aria-pressed="true"] {
  border-color: var(--color-support);
  color: var(--color-support);
}

/* --- 12.4 the wall --- */

/* Ten across and five deep - one page of the wall is 50 covers, which is
   PAGE_SIZE in book-list.js. A fixed count rather than auto-fill: the rows
   have to stay square with each other as the column narrows, and a wall that
   silently reflows to nine is a wall whose "50 on screen" stops meaning five
   rows. Packed to a 5px seam, because the jackets are still the page and the
   reading only appears over the one being looked at. */
.wall {
  display: grid;
  grid-template-columns: repeat(10, minmax(0, 1fr));
  gap: 5px;
}

.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--os-space-2);
  min-width: 0;
}

.tile__jacket {
  position: relative;
  display: block;
  border-radius: 2px;
  overflow: hidden;
  background: var(--color-surface-sunken);
}

.tile__jacket .cover {
  border-radius: 2px;
  box-shadow: none;
}

/* An outline drawn inside the jacket, so hovering one does not nudge its
   neighbours in a grid with a 5px seam. */
.tile__jacket:hover,
.tile__jacket:focus-visible {
  outline: 3px solid var(--color-text);
  outline-offset: -3px;
  z-index: 2;
}

/* The scrim. Only where there is a pointer to hover with: a phone gets the
   caption instead, which is the same block without this rule.

   Ten jackets across the container measure is a 7rem column, so everything
   here is set for that: the padding drops a step, the type stays small, and
   the evidence line is counts rather than a sentence. A scrim that has to be
   read at arm's length is the wrong control for a wall this dense. */
@media (hover: hover) and (min-width: 60rem) {
  .tile__meta {
    position: absolute;
    inset: 0;
    justify-content: flex-end;
    padding: var(--os-space-3);
    border-radius: 2px;
    background: var(--os-ink-surface);
    opacity: 0;
    pointer-events: none;
    transition: opacity 140ms ease;
    /* A short cover and a long claim can add up to more than the jacket. The
       block keeps its own sizes and loses the top of itself rather than
       shrinking every line to fit, which is what a flex column does by
       default and it crops mid-letter. */
    overflow: hidden;
  }

  .tile__meta > * { flex: 0 0 auto; }

  .tile:hover .tile__meta,
  .tile:focus-within .tile__meta {
    opacity: 0.96;
    pointer-events: auto;
  }

  /* Painted on navy now, so the whole block reads from the ink ramp. The
     title drops its two-line floor: the scrim sets from the bottom, so a
     short title costs an empty line rather than buying an alignment. */
  .tile__title {
    color: var(--os-ink-text);
    min-height: 0;
  }
  .tile__byline { color: var(--os-ink-text-muted); }
  .tile__claim {
    color: var(--os-ink-teal);
    font-size: var(--os-text-2xs);
    -webkit-line-clamp: 3;
  }
  .tile__evidence,
  .tile__thin { color: var(--os-ink-text-muted); }
  .tile__evidence { white-space: nowrap; }
  .tile .split { background: var(--os-ink-coral); }
  .tile .split__agree { background: var(--os-ink-teal); }

  /* Full width rather than hugging its label: at this column width "Want to
     read" wraps to two lines inside a shrink-to-fit button, and a two-line
     button on a jacket reads as a mistake. */
  .tile__shelf {
    align-self: stretch;
    padding: var(--os-space-1) var(--os-space-2);
    text-align: center;
    border-color: var(--os-ink-band);
    color: var(--os-ink-text);
  }

  .tile__shelf:hover { border-color: var(--os-ink-text); }

  .tile__shelf[aria-pressed="true"] {
    border-color: var(--os-ink-teal);
    color: var(--os-ink-teal);
  }
}

/* Rules either side of the button: the wall has no bottom edge of its own, so
   the control has to draw the end of the shelf. */
.wall-more {
  display: flex;
  align-items: center;
  gap: var(--os-space-4);
  margin-block-start: var(--os-space-6);
}

.wall-more::before,
.wall-more::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--color-border);
}

/* --- 12.5 the controls --- */

.filter-group__title {
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--color-support);
  margin-block-end: var(--os-space-3);
}

/* Genre, decade and rating live behind a disclosure at every width now. The
   moods in the bar above are the filter this page leads with; these are the
   ones a reader hunting a specific book opens on purpose. */
.filter-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--os-space-3);
  min-height: var(--os-tap-min);
  padding: var(--os-space-3) var(--os-space-4);
  background: var(--color-surface);
  border: var(--os-border-mid) solid var(--color-border-strong);
  border-radius: var(--os-radius-sm);
  color: var(--color-text);
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-semibold);
  text-align: start;
  cursor: pointer;
  transition: border-color var(--os-duration-fast) var(--os-ease-out);
}

.filter-toggle:hover,
.filter-toggle:active { border-color: var(--color-accent-border); }

.filter-toggle__count {
  display: inline-block;
  margin-inline-start: var(--os-space-2);
  padding-inline: var(--os-space-2);
  border-radius: var(--os-radius-sm);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-size: var(--os-text-xs);
  font-variant-numeric: tabular-nums;
}

.filter-toggle__icon {
  display: inline-flex;
  line-height: 0;
  color: var(--color-text-subtle);
}

/* Two glyphs swapped by state rather than one rotated: the brand's motion is
   colour and shadow, never movement. */
.filter-toggle[aria-expanded="false"] .filter-toggle__icon--open,
.filter-toggle[aria-expanded="true"] .filter-toggle__icon--closed {
  display: none;
}

.filter-panel {
  display: none;
  flex-direction: column;
  gap: var(--os-space-5);
}

.filter-panel.is-open { display: flex; }

/* The wall's variant: a strip above the covers rather than a rail beside them,
   boxed so the open panel reads as one surface over the grid. */
.filter-panel--flat.is-open {
  gap: var(--os-space-5);
  padding: var(--os-space-5);
  margin-block-end: var(--os-space-5);
  background: var(--color-surface);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
}

@media (min-width: 40.0625rem) {
  /* Tablet up: the open panel lays the groups out as a wrapped row. */
  .filter-panel.is-open { flex-flow: row wrap; }
  .filter-panel .filter-group { flex: 1 1 16rem; }
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--os-space-3);
  margin-block-end: var(--os-space-4);
}

/* Two rows on a phone: the search field on its own line, then the filter
   toggle and the sort select sharing the next one. The spacer that
   right-aligns the desktop toolbar is dropped, or it would absorb the second
   row's free space and shove both controls to the edge. */
.toolbar__spacer { display: none; }

.toolbar__sort {
  flex: 1 1 auto;
  min-width: 0;
}

.toolbar__search { flex: 1 1 100%; }

@media (min-width: 40.0625rem) {
  .toolbar__spacer {
    display: block;
    margin-inline-start: auto;
  }

  .toolbar__sort { flex: 0 0 auto; }

  /* Grows into the free space the spacer would otherwise claim, capped so the
     field does not stretch the full width of a wide results column. */
  .toolbar__search {
    flex: 1 1 18rem;
    max-width: 24rem;
  }
}

.active-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--os-space-2);
  margin-block-end: var(--os-space-4);
}

/* The search-query chip echoes whatever was typed, so it is the one chip that
   has to wrap instead of running off the screen. */
.active-filters .chip {
  max-width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: start;
}

/* --------------------------------------------- 13. Book details page -----
   Design direction 1b, "The Verdict". The page is a stack of full-bleed
   bands - navy hero, white ledger, sand cautions, navy fit, white
   neighbours, grey club band - and each band puts a .container inside
   itself. book.html therefore carries .page--bleed and no container of its
   own.

   Two conventions run through this section:

     - Anything painted on a navy band reads its colour from the --os-ink-*
       ramp in tokens.css, which is deliberately theme-invariant.
     - Bars and dials take their one measured proportion from an inline
       custom property (--fill, --band-start, --band-width, --dot) set by
       book-details.js. That is the only inline style on the page, and it
       carries data rather than design. */

/* --- 13.1 utility bar --- */

.verdict-bar {
  background: var(--os-ink-surface);
}

.verdict-bar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--os-space-4);
  padding-block: var(--os-space-3);
}

.verdict-bar__back {
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-bold);
  color: var(--os-coral-200);
  text-decoration: none;
}

.verdict-bar__back:hover { color: var(--os-ink-text); }

.verdict-bar__record {
  padding: var(--os-space-2) var(--os-space-3);
  border: var(--os-border-mid) solid var(--os-navy-600);
  border-radius: var(--os-radius-sm);
  background: none;
  color: var(--os-ink-text);
  font-family: inherit;
  font-size: var(--os-text-xs);
  font-weight: var(--os-weight-semibold);
  cursor: pointer;
  transition: border-color var(--os-duration-fast) var(--os-ease-out);
}

.verdict-bar__record:hover { border-color: var(--os-ink-text); }

/* --- 13.1b the sticky shelf bar ---
   The hero's jacket and its three buttons, pinned under the site header once
   they scroll away, with the fit score joining them as soon as the API has
   one. Navy, because it is the hero continuing rather than a new band, and it
   has to read as the same object it was a moment ago.

   `top` follows the header's measured height rather than a token: the header
   wraps to two rows below 40rem, and a hard 4rem would tuck this bar under it
   on exactly the screens where it matters most. ui.js publishes --header-h.

   Fixed rather than sticky, and the reason is the reveal. A sticky bar sits in
   the flow at its own position in the document - which is above the fold, so
   un-hiding it mid-scroll inserts its height above the reader's scroll
   position and the page lurches. Fixed takes it out of flow entirely: showing
   and hiding it costs the layout nothing at all. */
.shelfbar {
  position: fixed;
  inset-block-start: var(--header-h, var(--os-header-height));
  inset-inline: 0;
  z-index: var(--os-z-sticky);
  background: var(--os-ink-raised);
  border-block-end: var(--os-border-hair) solid var(--os-ink-rule);
}

.shelfbar[hidden] { display: none; }

.shelfbar__inner {
  display: flex;
  align-items: center;
  gap: var(--os-space-4);
  padding-block: var(--os-space-3);
}

.shelfbar__book {
  display: flex;
  align-items: center;
  gap: var(--os-space-3);
  min-width: 0;
  flex: 1 1 auto;
}

/* Small enough to be a reminder rather than a second hero. The jacket keeps
   its own aspect ratio, so this is a width and nothing else. */
.shelfbar__book .cover {
  width: 1.75rem;
  flex: 0 0 auto;
  border-radius: 2px;
}

.shelfbar__title {
  font-family: var(--os-font-display);
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-tight);
  color: var(--os-ink-text);
}

.shelfbar__byline {
  font-size: var(--os-text-xs);
  color: var(--os-ink-text-subtle);
}

/* The score, travelling. Coral on the numeral and nothing else: it is the one
   figure on the page that is about the reader, and the bar is otherwise all
   navy and sand. */
.shelfbar__fit {
  display: flex;
  align-items: baseline;
  gap: var(--os-space-2);
  padding-inline: var(--os-space-4);
  border-inline: var(--os-border-hair) solid var(--os-ink-rule);
  flex: 0 0 auto;
}

.shelfbar__fit[hidden] { display: none; }

.shelfbar__fitscore {
  font-family: var(--os-font-display);
  font-size: var(--os-text-lg);
  font-weight: var(--os-weight-bold);
  line-height: 1;
  letter-spacing: var(--os-tracking-tight);
  color: var(--os-ink-coral);
}

.shelfbar__fitlabel {
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--os-ink-text-subtle);
}

.shelfbar__actions {
  display: flex;
  gap: var(--os-space-2);
  flex: 0 0 auto;
}

.shelfbar__actions .btn {
  padding-inline: var(--os-space-3);
  font-size: var(--os-text-xs);
}

/* --- 13.2 the paper hero ---
   Direction 1b-ii. The cover sits large on the right with a drawn fore-edge
   and a deep shadow; the argument runs down the left and bottoms out level
   with the jacket, so the two read as one object on a shelf rather than as
   two stacked columns. */

.hero-paper {
  /* Lit from behind the jacket: the gradient's hot corner sits where the cover
     does, which is what stops a flat navy band from reading as a header bar. */
  background: radial-gradient(120% 90% at 78% 10%, var(--os-ink-band) 0%, var(--os-ink-surface) 60%);
  color: var(--os-ink-text-muted);
}

.hero-paper__inner {
  display: grid;
  /* Wide enough for the jacket the direction wants and for the three shelf
     buttons to sit on one row underneath it. */
  grid-template-columns: minmax(0, 1fr) clamp(13rem, 22vw, 19rem);
  align-items: end;
  gap: var(--os-space-8);
  padding-block: var(--os-space-7) var(--os-space-9);
}

.hero-paper__argument {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-5);
}

.hero-paper__title {
  margin: 0;
  font-size: var(--os-text-4xl);
  line-height: var(--os-leading-tight);
  letter-spacing: -0.03em;
  color: var(--os-ink-text);
  text-wrap: pretty;
}

.hero-paper__byline {
  font-size: var(--os-text-lg);
  color: var(--os-ink-text-muted);
}

/* The schtick sits between the byline and the corpus lede, and the type scale
   is the whole argument for the order: title, then the plain answer to "what
   is this", then what the reviews made of it. Rendered one sentence per line
   with a quiet arrow lead-in, so the setup, complication and reframe stay
   distinct while still reading as one thought.

   No styling on [data-voice]: a sober reframe and a dry one are the same
   register on the page, and marking which is which would be the page telling
   the reader when to find it funny. The attribute is there for a later
   decision, not for a colour. */
.hero-paper__schtick-lines {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: var(--os-space-2);
  max-width: 44ch;
  font-family: var(--os-font-display);
  font-size: var(--os-text-xl);
  line-height: var(--os-leading-snug);
  letter-spacing: -0.01em;
  color: var(--os-ink-text);
  text-wrap: pretty;
}

.hero-paper__schtick-line {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  gap: var(--os-space-2);
}

.hero-paper__schtick-line::before {
  content: "\2192";
  color: var(--os-ink-coral);
}

/* Small print on any navy band - the hero's shelf note and method note, and
   the fit panel's legend. One rule rather than three, because they are the
   same voice at the same size in the same place. */
.note-on-dark {
  margin: 0;
  font-size: var(--os-text-xs);
  line-height: var(--os-leading-normal);
  color: var(--os-ink-text-subtle);
  max-width: 62ch;
}

.hero-paper__shape {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-4);
  padding-block-start: var(--os-space-5);
  border-block-start: var(--os-border-hair) solid var(--os-ink-rule);
}

.hero-paper__shape-label {
  display: inline-flex;
  align-items: center;
  gap: var(--os-space-2);
}

/* Seven bars across two rows. Two rows of four rather than one column of
   seven: at column width the shape is a list to read down, and it is meant
   to be a silhouette taken in at a glance. */
.shape-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--os-space-4) var(--os-space-6);
  max-width: 44rem;
}

/* Under the jacket, in the aside. Four shelf states are shown as two even
   columns so the controls stay balanced and there is no orphaned fourth button
   on its own row. */
.hero-paper__actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--os-space-2);
}

/* The label is what sets the width here, so the buttons drop to the small
   caps size and give their padding back to the text. */
.hero-paper__actions .btn {
  padding-inline: var(--os-space-2);
  font-size: var(--os-text-xs);
  justify-content: center;
}

/* --- 13.2b the book as an object --- */

/* The jacket with the shelf actions under it. The column bottoms out level
   with the argument beside it, so the two read as one block rather than as a
   picture floating above a caption. */
.hero-paper__aside {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-4);
}

.hero-paper__object {
  position: relative;
}

.hero-paper__object .cover {
  border-radius: 3px 5px 5px 3px;
  box-shadow: 0 30px 64px rgba(0, 0, 0, 0.55);
}

/* --- 13.3 trait intensity bars --- */

/* One cell of the shape grid: the trait's name and its value share the top
   line, the bar runs the full width underneath. Laid out by area rather than
   by source order, so the markup can keep the reading order a screen reader
   wants (name, bar, number) while the eye gets the number next to the name. */
.shape-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "label value"
    "track track";
  align-items: baseline;
  gap: var(--os-space-2);
}

.shape-row__label {
  grid-area: label;
  font-size: var(--os-text-sm);
  color: var(--os-ink-text);
  overflow-wrap: anywhere;
}

.shape-row__track {
  grid-area: track;
  position: relative;
  height: 0.5rem;
  border-radius: var(--os-radius-bar);
  background: var(--os-ink-track);
  overflow: hidden;
}

/* The catalogue median for this trait. A hairline, not a second bar: it is the
   reference the fill is read against, so it must not compete with it. */
.shape-row__median {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--median, 50%);
  width: 1px;
  background: var(--os-ink-text-subtle);
  opacity: 0.7;
}

.shape-row__fill {
  display: block;
  width: var(--fill, 0%);
  height: 100%;
  border-radius: var(--os-radius-bar);
  background: var(--os-ink-teal);
}

.shape-row__value {
  grid-area: value;
  font-size: var(--os-text-xs);
  text-align: end;
  color: var(--os-ink-text-subtle);
}

/* --- 13.4 the ledger --- */

.legend {
  display: flex;
  gap: var(--os-space-4);
  font-size: var(--os-text-xs);
  color: var(--color-text-subtle);
}

.legend__key {
  display: flex;
  align-items: center;
  gap: var(--os-space-2);
}

.legend__swatch {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 2px;
}

.legend__swatch--agree { background: var(--color-support); }
.legend__swatch--dispute { background: var(--color-accent); }

.ledger {
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
  background: var(--color-surface);
  box-shadow: var(--os-shadow-sm);
  overflow: hidden;
}

.ledger__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--os-space-5);
  padding: var(--os-space-5);
}

.ledger__row + .ledger__row {
  border-block-start: var(--os-border-hair) solid var(--color-divider);
}

.ledger__claim {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-2);
}

.ledger__title {
  font-family: var(--os-font-display);
  font-size: var(--os-text-md);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-tight);
  color: var(--color-text);
}

.ledger__gloss {
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-normal);
  color: var(--color-text-muted);
}

.ledger__evidence {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-3);
}

/* Agreement runs teal from the start of the bar; whatever is left is the
   dispute, in coral. One element, two colours, no second box to keep in sync. */
.split {
  display: block;
  height: 0.5rem;
  border-radius: var(--os-radius-bar);
  background: var(--color-accent);
  overflow: hidden;
}

.split__agree {
  display: block;
  width: var(--fill, 0%);
  height: 100%;
  background: var(--color-support);
}

.ledger__counts {
  display: flex;
  justify-content: space-between;
  gap: var(--os-space-3);
  font-size: var(--os-text-xs);
  color: var(--color-text-subtle);
}

.ledger__quote {
  margin: 0;
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-normal);
  font-style: italic;
  color: var(--color-text-muted);
}

/* --- 13.5 cautions --- */

.cautions {
  margin-block-start: var(--os-space-9);
  padding: var(--os-space-6);
  border-radius: var(--os-radius-xl);
  background: var(--color-surface-warm);
}

.cautions__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--os-space-4);
}

/* Most books clear one caution, and auto-fit stretched that single card across
   the full measure - a sentence of warning set as a banner. A lone card takes
   a card's width and leaves the rest of the band alone. */
.cautions__grid:has(> :only-child) {
  grid-template-columns: minmax(0, 32rem);
}

.caution {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-2);
  padding: var(--os-space-5);
  border-radius: var(--os-radius-md);
  background: var(--color-surface);
}

.caution__tag {
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
}

.caution--contested .caution__tag { color: var(--color-accent); }
.caution--support .caution__tag { color: var(--color-support); }
.caution--neutral .caution__tag { color: var(--color-text-muted); }

.caution__title {
  font-family: var(--os-font-display);
  font-size: var(--os-text-md);
  font-weight: var(--os-weight-semibold);
  color: var(--color-text);
}

.caution__body {
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-normal);
  color: var(--color-text-muted);
}

/* --- 13.6 the case for and against --- */

/* Two readings across one seam. There is no card here on purpose: a pair of
   panels reads as a comparison table, and the section is an argument. The
   middle track is a single hairline both sides hang off. */
.cases {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
  column-gap: var(--os-space-7);
  /* The title states the two claims; the seam then argues them. Without this
     the heading sits on top of the "For" label and reads as its first line. */
  margin-block-start: var(--os-space-7);
}

/* One-sided books get one reading and no seam. Capped rather than full-bleed:
   a lone column stretched across the full measure reads as a banner, not as
   one half of an argument that happens to have no other half. */
.cases--single {
  grid-template-columns: minmax(0, 1fr);
  max-width: 34rem;
}

/* Teal at the top, coral at the bottom - the two sides are named by the ends
   of the one rule they share, so neither is tinted as the right answer. The
   label inside is for the narrow layout only. */
.cases__seam {
  background: linear-gradient(
    var(--color-support),
    var(--color-divider) 40%,
    var(--color-divider) 60%,
    var(--color-accent)
  );
}

.cases__seam span { display: none; }

.case {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-4);
}

/* The right-hand reading is set right-aligned, so the two press on the seam
   from either side rather than drifting off it. */
.case--against { text-align: right; }

.case__label {
  display: flex;
  align-items: baseline;
  gap: var(--os-space-2);
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--os-tracking-caps);
}

.case--for .case__label { color: var(--color-support); }

.case--against .case__label {
  color: var(--color-accent);
  flex-direction: row-reverse;
}

.case__label b {
  font-family: var(--os-font-display);
  font-size: var(--os-text-xl);
  font-weight: var(--os-weight-bold);
  letter-spacing: var(--os-tracking-tight);
  line-height: 1;
  text-transform: none;
}

.case__summary {
  margin: 0;
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-relaxed);
  color: var(--color-text);
}

.case__quotes {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.case__quote {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-1);
}

/* Each quote hangs off its own side of the seam on a short rule of its side's
   colour, so a sentence is placed by where it sits, not by a label. */
.case--for .case__quote {
  border-inline-start: 2px solid var(--color-support-border);
  padding-inline-start: var(--os-space-4);
}

.case--against .case__quote {
  border-inline-end: 2px solid var(--color-accent-border);
  padding-inline-end: var(--os-space-4);
}

.case__quote q {
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-normal);
  font-style: italic;
  color: var(--color-text-muted);
}

.case__source {
  font-size: var(--os-text-2xs);
  color: var(--color-text-subtle);
}

/* --- 13.7 fit for you --- */

.fit {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--os-space-7);
  padding: var(--os-space-6);
  border-radius: var(--os-radius-xl);
  background: var(--os-ink-surface);
}

/* Nobody signed in, or too little read to measure against: one column, no
   score, no axes. The panel keeps its dark card so the page's rhythm does not
   change depending on who is looking at it. */
.fit--single {
  grid-template-columns: minmax(0, 1fr);
}

.fit__verdict {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-4);
}

/* The score used to be a conic-gradient ring with a navy plug in the middle.
   Two reasons it is not any more. The radius ceiling is 8px, and a ring is a
   999px corner by definition - there is no honest way to draw one under the
   rule. And the ring was the most generic thing on the page: every product
   dashboard has one, and it made a number the engine works hard for look like
   a battery indicator. A numeral at display size, with the same value read a
   second way underneath it, is both quieter and harder to mistake for
   somebody else's UI. */
.fit__reading {
  display: grid;
  grid-template-columns: minmax(0, 10rem) minmax(0, 1fr);
  gap: var(--os-space-5);
  align-items: start;
}

.fit__gauge {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-1);
}

.fit__score {
  font-family: var(--os-font-display);
  font-size: clamp(3rem, 2.2rem + 3vw, 4.25rem);
  font-weight: var(--os-weight-bold);
  line-height: 0.82;
  letter-spacing: -0.04em;
  color: var(--os-ink-text);
}

/* Waiting. Two hyphens holding the numeral's place, set in the colour of the
   track rather than the colour of an answer, so the shape of the panel is
   already right when the real figure lands and nothing on the page moves. */
.fit__score--waiting {
  color: var(--os-ink-track);
  letter-spacing: -0.02em;
}

.fit__scorelabel {
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--os-ink-text-subtle);
}

/* The same number again, as a length. Redundant on purpose: a numeral is
   precise and a bar is comparable, and the panel is asking the reader to do
   both - read the score, then feel where it sits. */
.fit__meter {
  display: block;
  height: 0.375rem;
  margin-block-start: var(--os-space-2);
  border-radius: var(--os-radius-bar);
  background: var(--os-ink-track);
  overflow: hidden;
}

.fit__meterfill {
  display: block;
  width: var(--fill, 0%);
  height: 100%;
  border-radius: var(--os-radius-bar);
  background: var(--os-coral-500);
}

.fit__headline {
  font-family: var(--os-font-display);
  font-size: var(--os-text-lg);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-tight);
  color: var(--os-ink-text);
}

.fit__blurb,
.fit__caveat {
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-normal);
  color: var(--os-ink-text-muted);
}

.fit__caveat {
  display: flex;
  gap: var(--os-space-3);
  align-items: flex-start;
  padding: var(--os-space-4);
  border-radius: var(--os-radius-md);
  background: var(--os-ink-raised);
}

.fit__dot {
  flex: 0 0 auto;
  margin-block-start: 0.4rem;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--os-radius-bar);
  background: var(--os-coral-500);
}

.fit__axes {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-3);
}

/* --- 13.8 fit axes --- */

.axis {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-2);
}

.axis__head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--os-space-2);
}

.axis__label {
  font-size: var(--os-text-sm);
  color: var(--os-ink-text);
}

.axis__note {
  font-size: var(--os-text-2xs);
  color: var(--os-ink-text-subtle);
}

/* The colour follows what the miss costs, not how far out it is. "over" is a
   surplus on an axis where surplus is forgiven - twistier than your usual is
   the thing you keep reaching for - so it reads teal alongside "inside".
   "shade" and "under" are mild and read sand. Coral is kept for "far", which
   only happens on tone, demand or form, where going past a reader's range is
   not a favour: it is the one row on the panel the eye should catch. */
.axis__note--over,
.axis__note--inside { color: var(--os-ink-teal); }
.axis__note--shade,
.axis__note--under { color: var(--os-sand-500); }
.axis__note--far { color: var(--os-ink-coral); }

.axis__track {
  position: relative;
  height: 0.5rem;
  border-radius: var(--os-radius-bar);
  background: var(--os-ink-track);
}

/* The band is where the reader's own books usually sit; the dot is this one. */
.axis__band {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--band-start, 0%);
  width: var(--band-width, 0%);
  border-radius: var(--os-radius-bar);
  background: var(--os-ink-band);
}

.axis__dot {
  position: absolute;
  inset-block-start: -0.1875rem;
  inset-inline-start: var(--dot, 0%);
  margin-inline-start: -0.4375rem;
  width: 0.875rem;
  height: 0.875rem;
  border-radius: var(--os-radius-bar);
  border: var(--os-border-thick) solid var(--os-ink-surface);
  background: var(--os-ink-teal);
}

.axis__dot--far { background: var(--os-coral-500); }

/* --- 13.9 nearest neighbours --- */

/* Three across, so the six neighbours read as two even rows. auto-fit would
   fit four on a wide screen and leave a ragged 4 + 2, which reads as a wrap
   rather than a set. */
.neighbour-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--os-space-4);
}

/* flex-start, not the default stretch: a stretched cover fills the card's
   height and overrides its own aspect-ratio, cropping the jacket into a tall
   strip. */
.neighbour {
  display: flex;
  align-items: flex-start;
  gap: var(--os-space-4);
  padding: var(--os-space-4);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
  background: var(--color-surface);
  color: inherit;
  text-decoration: none;
  transition: box-shadow var(--os-duration-fast) var(--os-ease-out),
    border-color var(--os-duration-fast) var(--os-ease-out);
}

.neighbour:hover {
  border-color: var(--color-border-strong);
  box-shadow: var(--os-shadow-md);
}

.neighbour .cover {
  width: 3.5rem;
  flex: 0 0 auto;
}

.neighbour__body {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-1);
  min-width: 0;
}

.neighbour__title {
  font-family: var(--os-font-display);
  font-size: var(--os-text-md);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-tight);
  color: var(--color-text);
}

.neighbour__byline {
  font-size: var(--os-text-xs);
  color: var(--color-text-subtle);
}

.neighbour__verdict {
  margin-block-start: var(--os-space-1);
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--color-support);
}

.neighbour__differs {
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-normal);
  color: var(--color-text-muted);
}

/* --- 13.10 club band --- */

.club {
  margin-block-start: var(--os-space-9);
  background: var(--color-surface-sunken);
  border-block-start: var(--os-border-hair) solid var(--color-border);
}

.club__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--os-space-4);
  padding-block: var(--os-space-7);
}

.club__line {
  font-family: var(--os-font-display);
  font-size: var(--os-text-xl);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-tight);
  color: var(--color-text);
  max-width: 44ch;
}

/* --- 13.11 the record dialog --- */

.record {
  position: fixed;
  inset: 0;
  z-index: var(--os-z-overlay);
  display: grid;
  place-items: center;
  padding: var(--os-space-4);
}

.record[hidden] { display: none; }

.record__scrim {
  position: absolute;
  inset: 0;
  background: var(--color-scrim);
}

.record__panel {
  position: relative;
  width: 100%;
  max-width: 34rem;
  max-height: 85vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--os-space-5);
  padding: var(--os-space-6);
  border-radius: var(--os-radius-lg);
  background: var(--color-surface);
  box-shadow: var(--os-shadow-lg);
}

.record__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--os-space-4);
}

.record__title {
  margin: 0;
  font-size: var(--os-text-xl);
}

.record__close {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: 0;
  border-radius: var(--os-radius-sm);
  background: none;
  color: var(--color-text-subtle);
  cursor: pointer;
}

.record__close:hover {
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

.record__blurb {
  margin: 0;
  font-size: var(--os-text-sm);
  line-height: var(--os-leading-relaxed);
  color: var(--color-text-muted);
}

.record .meta-grid {
  margin: 0;
  gap: var(--os-space-4);
}

.record__source {
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-bold);
  color: var(--color-accent);
  text-decoration: none;
}

.record__source:hover { color: var(--color-accent-hover); }

/* --- 13.12 metadata grid, shared by the record dialog --- */

.meta-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--os-space-5);
}

.meta-grid__label {
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-caps);
  text-transform: uppercase;
  color: var(--color-text-subtle);
  margin-block-end: var(--os-space-1);
}

.meta-grid__value {
  font-size: var(--os-text-md);
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

/* Kept for the profile page's placeholder copy. */
.review__foot {
  font-size: var(--os-text-xs);
  color: var(--color-text-subtle);
}

/* --- 13.13 the placeholder flag --------------------------------------------
   This page renders several things the catalogue cannot supply yet. Every one
   of them is marked in the UI rather than quietly faked - the honesty rule in
   docs/03-data-sources-as-built.md applies to layout work too. Remove the
   chip's callers as each gap in docs/gap-in-book-details-audit.md closes. */

.stub-flag {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-inline-start: var(--os-space-2);
  padding: 0.1rem var(--os-space-2);
  border: var(--os-border-hair) dashed currentColor;
  border-radius: var(--os-radius-pill);
  font-family: var(--os-font-body);
  font-size: var(--os-text-2xs);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0.75;
}

/* -------------------------------------------------- 14. Profile page ----- */

.profile-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 20rem;
  gap: var(--os-space-8);
  align-items: start;
}

.profile-card {
  padding: var(--os-space-6);
  background: var(--color-surface);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-lg);
  box-shadow: var(--os-shadow-sm);
}

.profile-head {
  display: flex;
  align-items: center;
  gap: var(--os-space-5);
  flex-wrap: wrap;
  margin-block-end: var(--os-space-6);
}

/* Flat teal circle with initials, per the brand's avatar treatment. */
.avatar {
  width: 4.5rem;
  height: 4.5rem;
  border-radius: var(--os-radius-pill);
  display: grid;
  place-items: center;
  background: var(--color-support);
  color: var(--color-text-on-accent);
  font-family: var(--os-font-body);
  font-size: var(--os-text-2xl);
  font-weight: var(--os-weight-semibold);
  letter-spacing: 0.02em;
  flex-shrink: 0;
}

.avatar--sm {
  width: 2.25rem;
  height: 2.25rem;
  font-size: var(--os-text-md);
}

.tabs {
  display: flex;
  gap: var(--os-space-1);
  border-block-end: var(--os-border-hair) solid var(--color-border);
  margin-block-end: var(--os-space-6);
  overflow-x: auto;
  scrollbar-width: none;
}

.tabs__tab {
  padding: var(--os-space-3) var(--os-space-4);
  border: 0;
  border-block-end: var(--os-border-thick) solid transparent;
  background: none;
  color: var(--color-text-muted);
  font-size: var(--os-text-md);
  font-weight: var(--os-weight-medium);
  cursor: pointer;
  white-space: nowrap;
  margin-block-end: calc(var(--os-border-hair) * -1);
  transition: color var(--os-duration-fast) var(--os-ease-out),
    border-color var(--os-duration-fast) var(--os-ease-out);
}

.tabs__tab:hover,
.tabs__tab:active { color: var(--color-text); }

.tabs__tab[aria-selected="true"] {
  color: var(--color-accent);
  border-block-end-color: var(--color-accent);
}

/* The Auth0 wiring is deliberately not implemented; this panel documents
   what will replace the demo session. */
.auth-panel {
  padding: var(--os-space-6);
  border: var(--os-border-thick) dashed var(--color-border-strong);
  border-radius: var(--os-radius-lg);
  background: var(--color-surface);
  display: flex;
  flex-direction: column;
  gap: var(--os-space-4);
}

.auth-panel__badge {
  align-self: flex-start;
}

.code-note {
  font-family: var(--os-font-mono);
  font-size: var(--os-text-xs);
  line-height: var(--os-leading-relaxed);
  color: var(--color-text-muted);
  background: var(--color-surface-sunken);
  border: var(--os-border-hair) solid var(--color-border);
  border-radius: var(--os-radius-sm);
  padding: var(--os-space-4);
  overflow-x: auto;
  white-space: pre;
}

.checklist {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-3);
}

.checklist__item {
  display: flex;
  align-items: flex-start;
  gap: var(--os-space-3);
  font-size: var(--os-text-sm);
  color: var(--color-text-muted);
}

.checklist__mark {
  flex-shrink: 0;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: var(--os-radius-bar);
  border: var(--os-border-hair) solid var(--color-border-strong);
  display: grid;
  place-items: center;
  font-size: var(--os-text-2xs);
  color: var(--color-text-subtle);
  margin-block-start: 0.15rem;
}

.checklist__item[data-done="true"] .checklist__mark {
  background: var(--color-support);
  border-color: var(--color-support);
  color: var(--color-text-inverse);
}

/* ------------------------------------------- 15. Adjective poll page ----- */

/* The ballot is a plain checkbox form. Every word is a <label> wrapping a
   real <input>, so selection, keyboard focus and the accessible name are all
   native; the CSS only hides the box and paints the chip around it. Nothing
   here needs script. */

.poll-layout {
  display: grid;
  gap: var(--os-space-8);
  align-items: start;
}

.poll-head { max-width: 46ch; }

.poll-form { margin-block-start: var(--os-space-2); }

/* Fieldsets carry a border and inline padding by default in every browser,
   and the design wants a plain block. */
.ballot {
  min-inline-size: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.ballot__legend {
  display: flex;
  flex-direction: column;
  gap: var(--os-space-1);
  padding: 0;
  margin-block-end: var(--os-space-4);
}

.ballot__legendtitle {
  font-family: var(--os-font-display);
  font-size: var(--os-text-xl);
  font-weight: var(--os-weight-semibold);
  letter-spacing: var(--os-tracking-tight);
  color: var(--color-text);
}

.ballot__hint {
  font-size: var(--os-text-sm);
  color: var(--color-text-subtle);
  max-width: 52ch;
}

.ballot__options {
  display: flex;
  flex-wrap: wrap;
  gap: var(--os-space-2);
}

.ballot__option {
  display: inline-flex;
  align-items: center;
  gap: var(--os-space-2);
  padding: var(--os-space-2) var(--os-space-4);
  /* A card fill, not the chip component's sunken one: the ballot sits
     directly on the canvas, and --color-surface-sunken is the canvas. */
  border: var(--os-border-mid) solid var(--color-border);
  border-radius: var(--os-radius-pill);
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: var(--os-text-sm);
  font-weight: var(--os-weight-semibold);
  cursor: pointer;
  user-select: none;
  transition: background var(--os-duration-fast) var(--os-ease-out),
    border-color var(--os-duration-fast) var(--os-ease-out),
    color var(--os-duration-fast) var(--os-ease-out);
}

.ballot__option:hover,
.ballot__option:active {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* The input keeps its size and position so the focus ring lands on the chip
   and the control stays hit-testable; only its paint is suppressed. */
.ballot__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The tick box. Always present so ticking a word never reflows the row. */
.ballot__mark {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: 1rem;
  height: 1rem;
  border-radius: var(--os-radius-xs);
  border: var(--os-border-mid) solid var(--color-border-strong);
  background: var(--color-surface);
  color: transparent;
  transition: background var(--os-duration-fast) var(--os-ease-out),
    border-color var(--os-duration-fast) var(--os-ease-out),
    color var(--os-duration-fast) var(--os-ease-out);
}

/* Chosen words read as teal structure, not coral action: a reader can pick
   five of these at once, and coral is rationed to the one submit button. */
.ballot__option:has(.ballot__input:checked) {
  background: var(--color-support-soft);
  border-color: var(--color-support-border);
  color: var(--color-support);
}

.ballot__input:checked + .ballot__mark {
  background: var(--color-support);
  border-color: var(--color-support);
  color: var(--color-text-on-accent);
}

.ballot__option:has(.ballot__input:focus-visible) {
  outline: var(--os-border-thick) solid var(--color-focus);
  outline-offset: 2px;
}

/* The avoid block is a caution, so it borrows the coral tint rather than the
   solid fill. Same rule: tint and text, never a coral field. */
.ballot--avoid .ballot__option:has(.ballot__input:checked) {
  background: var(--color-accent-soft);
  border-color: var(--color-accent-border);
  color: var(--color-accent-active);
}

.ballot--avoid .ballot__input:checked + .ballot__mark {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
}

.ballot--write-in .ballot__options,
.ballot--write-in .field + .field {
  margin-block-start: var(--os-space-4);
}

.ballot--write-in .input { max-width: 24rem; }

.poll-submit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--os-space-4);
  padding-block-start: var(--os-space-5);
  border-block-start: var(--os-border-hair) solid var(--color-divider);
}

.poll-submit .review__foot { margin: 0; }

/* Fallback for browsers without :has(). The chip keeps its resting fill and
   the tick box alone carries the state, which is still unambiguous. */
@supports not selector(:has(*)) {
  .ballot__input:focus-visible + .ballot__mark {
    outline: var(--os-border-thick) solid var(--color-focus);
    outline-offset: 2px;
  }
}

@media (min-width: 60.0625rem) {
  .poll-layout { grid-template-columns: minmax(0, 1fr) 20rem; }
}

/* --------------------------------- 16. Loading, empty and error states --- */

.skeleton {
  background: linear-gradient(
    90deg,
    var(--color-surface-sunken) 25%,
    var(--color-surface) 50%,
    var(--color-surface-sunken) 75%
  );
  background-size: 200% 100%;
  animation: os-shimmer 1.4s var(--os-ease-in-out) infinite;
  border-radius: var(--os-radius-sm);
}

.skeleton--cover { aspect-ratio: 2 / 3; border-radius: var(--os-radius-sm); }
.skeleton--line { height: 0.75rem; }
.skeleton--line-short { height: 0.75rem; width: 60%; }

@keyframes os-shimmer {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}

.state {
  padding: var(--os-space-9) var(--os-space-6);
  text-align: center;
  border: var(--os-border-hair) dashed var(--color-border-strong);
  border-radius: var(--os-radius-lg);
  color: var(--color-text-muted);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--os-space-3);
}

.state__title {
  font-size: var(--os-text-xl);
  color: var(--color-text);
}

.state__body { max-width: 44ch; }

.notice {
  padding: var(--os-space-4) var(--os-space-5);
  border-radius: var(--os-radius-md);
  border: var(--os-border-hair) solid var(--color-accent-border);
  background: var(--color-accent-soft);
  color: var(--color-text);
  font-size: var(--os-text-sm);
}

/* ----------------------------------- 17. Utilities and accessibility ----- */

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

.skip-link {
  position: absolute;
  inset-inline-start: var(--os-space-4);
  inset-block-start: calc(var(--os-space-4) * -6);
  z-index: var(--os-z-overlay);
  padding: var(--os-space-3) var(--os-space-4);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  border-radius: var(--os-radius-sm);
  text-decoration: none;
  transition: inset-block-start var(--os-duration-fast) var(--os-ease-out);
}

.skip-link:focus {
  inset-block-start: var(--os-space-4);
  color: var(--color-text-on-accent);
}

.is-hidden { display: none !important; }

/* ---------------------------------------------------------- responsive --- */

/* Eight jackets across from here down. The wall stops growing at the container
   cap, so the column count stops changing there too: below it the row narrows
   with the viewport, and by this width ten covers are small enough that the
   title printed on one stops being legible - which is the first thing the wall
   asks of you. */
@media (max-width: 78rem) {
  .wall { grid-template-columns: repeat(8, minmax(0, 1fr)); }
}

@media (max-width: 60rem) {
  /* ---------------------------------------------- the nav moves to the thumb
     Below this width the capsule stops floating and the links leave it: the
     header becomes a slim solid strip carrying the brand alone, and the same
     <nav> is fixed at the bottom of the screen as a floating tab bar, with
     search riding down as a tab.

     The strip stays solid and sticky rather than floating like the mock's,
     because two sticky bars on the book page - the shelf bar, the mood bar -
     are positioned to start underneath it and need an opaque edge to meet. */
  .site-header {
    padding-block: 0;
    pointer-events: auto;
    background: var(--color-surface);
    border-block-end: var(--os-border-hair) solid var(--color-border);
  }

  .site-header__inner { max-width: var(--os-container-lg); }

  /* The lockup is the only thing left in the strip, so it sets the height:
     a hair smaller than the capsule's, with room to breathe either side. */
  .brand__logo { height: 2.25rem; }

  .capsule {
    min-height: 3.5rem;
    padding-inline: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .cmd { display: none; }

  .nav {
    position: fixed;
    inset-inline: var(--os-space-4);
    inset-block-end: calc(var(--os-space-4) + env(safe-area-inset-bottom, 0px));
    z-index: var(--os-z-header);
    margin-inline-start: 0;
    display: flex;
    align-items: stretch;
    gap: var(--os-space-1);
    padding: var(--os-space-2);
    background: var(--color-surface-raised);
    border: var(--os-border-hair) solid var(--color-border);
    border-radius: var(--os-radius-lg);
    box-shadow: var(--os-shadow-lg);
  }

  .nav__link,
  .nav__link--search {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: var(--os-space-1) var(--os-space-1) 0.3125rem;
    border: 0;
    background: none;
    font-family: inherit;
    font-size: var(--os-text-2xs);
    font-weight: var(--os-weight-semibold);
    color: var(--color-text-subtle);
    cursor: pointer;
  }

  .nav__icon {
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 1.75rem;
    border-radius: var(--os-radius-sm);
    color: var(--color-text-muted);
    transition: background var(--os-duration-fast) var(--os-ease-out),
      color var(--os-duration-fast) var(--os-ease-out);
  }

  /* The one coral on the bar, and the direction's own marker: the active tab
     carries it behind the icon while the label stays ink. */
  .nav__link[aria-current="page"] {
    background: none;
    color: var(--color-text);
  }

  .nav__link[aria-current="page"] .nav__icon {
    background: var(--color-accent);
    color: var(--color-text-on-accent);
  }

  .nav__link:hover,
  .nav__link:active {
    background: none;
    color: var(--color-text);
  }

  /* The tab bar floats over the foot of the page, so the document ends above
     it - otherwise the last row of the footer sits under the bar for good. */
  body {
    padding-block-end: calc(5.25rem + env(safe-area-inset-bottom, 0px));
  }

  /* The palette becomes a sheet at the thumb end, where the tab that opens it
     is. It keeps the panel's shape language; only the anchor moves. */
  .palette {
    align-items: flex-end;
    padding: 0;
  }

  .palette__panel {
    max-width: none;
    margin-block-start: 0;
    max-height: 80dvh;
    border-inline: 0;
    border-block-end: 0;
    border-radius: var(--os-radius-lg) var(--os-radius-lg) 0 0;
    padding-block-end: env(safe-area-inset-bottom, 0px);
  }

  .hero {
    grid-template-columns: 1fr;
    gap: var(--os-space-7);
  }

  .hero__shelf { max-width: 26rem; }

  /* minmax(0, ...) rather than a bare 1fr: a bare fr track takes the widest
     child's min-content as its floor, so one unbreakable line inside the
     column - a code sample, a nowrap button row - would widen the track past
     the viewport and take the whole page sideways with it. */
  .profile-layout {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--os-space-6);
  }

  /* Below this width the jacket's reading is a caption rather than a scrim -
     see 12.3 - so the covers need room for four or five lines of type under
     them, and the wall drops to four across. */
  .wall { grid-template-columns: repeat(4, minmax(0, 1fr)); }

  /* The book detail page collapses to one column: the fit panel stacks its
     reading above its axes, and the ledger stops splitting claim from evidence. */
  .fit,
  .ledger__row {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--os-space-5);
  }

  /* The seam folds. Both readings run down one rail that changes colour at
     the turn, and the seam itself becomes the divider that names it - the
     quotes drop their own rules, because the side is now carried by the
     block they sit in rather than by each sentence. */
  .cases {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--os-space-5);
  }

  .case {
    border-inline-start: 2px solid var(--color-support-border);
    padding-inline-start: var(--os-space-4);
  }

  .case--against {
    text-align: start;
    border-inline-start-color: var(--color-accent-border);
  }

  .case--against .case__label { flex-direction: row; }

  .case--for .case__quote,
  .case--against .case__quote {
    border: 0;
    padding: 0;
  }

  .cases__seam {
    display: flex;
    align-items: center;
    gap: var(--os-space-3);
    background: none;
  }

  .cases__seam::before,
  .cases__seam::after {
    content: "";
    flex: 1;
    border-block-start: var(--os-border-hair) solid var(--color-divider);
  }

  .cases__seam span {
    display: inline;
    font-size: var(--os-text-2xs);
    font-weight: var(--os-weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--os-tracking-caps);
    color: var(--color-text-subtle);
  }

  .hero-paper__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--os-space-6);
  }

  /* The jacket and its actions go above the argument rather than beside it.
     The object still comes first, because that is the direction's whole idea:
     you are looking at a book before you are reading about it. Only the jacket
     is narrowed: the buttons and the note take the full column, or they wrap
     into a thin ribbon beside a lot of empty navy. */
  .hero-paper__aside {
    order: -1;
    align-items: stretch;
  }

  .hero-paper__object {
    width: clamp(9rem, 40vw, 13rem);
  }

  /* Four bars a row needs the width for four. Two rows of four becomes four
     rows of two rather than a ragged wrap. */
  .shape-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* The author goes first out of the sticky bar: the jacket and the title
     already say which book this is. */
  .shelfbar__byline { display: none; }

  /* Two rows of three needs the width for three. Below it, two rows of two
     plus a third of two - still even, still not a ragged wrap. */
  .neighbour-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 40rem) {
  /* Four tabs across a 360px phone. The bar takes the tighter inset here, but
     needs no other help - four tabs leave about 80px each, which is more room
     than the widest label asks for. */
  .nav { inset-inline: var(--os-space-3); }

  .book-grid { grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); }

  /* Three jackets across a phone. The tiles carry their caption here, so a
     fourth column would leave the title clamped to two words. */
  .wall { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  .hero-paper__inner { padding-block: var(--os-space-5) var(--os-space-7); }

  /* One bar per row: at 360px a two-column shape grid leaves the trait names
     wrapping onto three lines each. */
  .shape-grid { grid-template-columns: minmax(0, 1fr); }

  .cautions,
  .fit {
    padding: var(--os-space-5);
    border-radius: var(--os-radius-lg);
  }

  /* The numeral and the verdict it belongs to stop sharing a row: at this
     width the two columns leave the headline wrapping every other word. */
  .fit__reading {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--os-space-4);
  }

  .neighbour-grid { grid-template-columns: minmax(0, 1fr); }

  /* At this width the three shelf buttons need the whole row, so the bar drops
     the book it is about. That is not a loss: the reader is on the book's own
     page, and the bar exists to keep the controls reachable, not to label
     them. The fit score stays - it is the one thing here that is not repeated
     anywhere above. */
  .shelfbar__book { display: none; }

  .shelfbar__fit {
    padding-inline: 0 var(--os-space-4);
    border-inline: 0;
    border-inline-end: var(--os-border-hair) solid var(--os-ink-rule);
  }

  .shelfbar__actions { flex: 1 1 auto; }
  .shelfbar__actions .btn { flex: 1 1 0; padding-inline: var(--os-space-2); }
}

/* ------------------------------------------------------------ touch input --
   On a touch pointer every control atom gets at least --os-tap-min of hit
   area. Scoped to `pointer: coarse` so mouse and trackpad keep the tighter
   density the design was drawn at. */

@media (pointer: coarse) {
  /* A tablet wide enough to keep the capsule still taps with a finger. */
  .cmd { height: var(--os-tap-min); }

  .palette__option,
  .palette__close {
    min-height: var(--os-tap-min);
  }

  .nav__link,
  .chip,
  .ballot__option,
  .tabs__tab,
  .btn,
  .brand,
  .section__link,
  .verdict-bar__back,
  .verdict-bar__record,
  .record__close,
  .site-footer__links a {
    min-height: var(--os-tap-min);
  }

  .verdict-bar__back,
  .verdict-bar__record {
    display: inline-flex;
    align-items: center;
  }

  /* Only the linked pills are targets; the plain variant is a label. */
  a.pill {
    min-height: var(--os-tap-min);
  }

  .section__link,
  .site-footer__links a {
    display: inline-flex;
    align-items: center;
  }

  /* An inline text link in a paragraph cannot take a min-height without
     breaking the line box, so only the standalone back link is padded out. */
  .link-back {
    min-height: var(--os-tap-min);
    align-items: center;
  }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

@media print {
  .site-header,
  .palette,
  .site-footer,
  .mood-bar,
  .toolbar,
  .verdict-bar,
  .club,
  .record { display: none; }

  /* No tab bar on paper, so the room reserved for it goes back. */
  body { background: #fff; color: #000; padding-block-end: 0; }

  /* The navy bands are ink on paper: printing them solid wastes a cartridge
     and reads worse than the plain page. The descendants each set their own
     on-inverse colour, so they have to be reset too. */
  .hero-paper,
  .hero-paper *,
  .fit,
  .fit * {
    background: #fff;
    color: #000;
  }
}
