
/* ------------------------------------------------------------------- fonts */
/* Self-hosted, and the reason matters. These five typefaces are not decoration: choosing one
   is a feature of a quote card, so a card rendered without them is a card rendered wrong. On
   Google Fonts that made the app's own content depend on a third party being reachable, which
   broke the project's no-third-party-runtime rule and would have made server-side export
   impossible, since a rasteriser cannot use a font it has to fetch from somewhere else.
   Sixteen latin woff2 files, and only the faces the app actually draws.
   font-display: swap so text is readable while they load. */

@font-face {
  font-family: 'Poltawski Nowy';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/fonts/poltawski-nowy-700-normal.woff2') format('woff2');
}
@font-face {
  font-family: "EB Garamond";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/eb-garamond-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "EB Garamond";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/eb-garamond-500-normal.woff2") format("woff2");
}
@font-face {
  font-family: "EB Garamond";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/eb-garamond-400-italic.woff2") format("woff2");
}
@font-face {
  font-family: "EB Garamond";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/eb-garamond-600-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/source-sans-3-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/source-sans-3-500-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/source-sans-3-600-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Libre Baskerville";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/libre-baskerville-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Libre Baskerville";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/libre-baskerville-400-italic.woff2") format("woff2");
}
@font-face {
  font-family: "Crimson Pro";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/crimson-pro-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Crimson Pro";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/crimson-pro-400-italic.woff2") format("woff2");
}
@font-face {
  font-family: "Zilla Slab";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/zilla-slab-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Zilla Slab";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/zilla-slab-500-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Spectral";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("/fonts/spectral-300-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Spectral";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/spectral-400-normal.woff2") format("woff2");
}
@font-face {
  font-family: "Spectral";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/spectral-400-italic.woff2") format("woff2");
}

:root {
  color-scheme: light dark;

  --paper:   #faf7f2;
  --surface: #ffffff;
  --sunk:    #f2ede4;
  --ink:     #1c1a17;
  /* The three text greys, measured rather than picked. Every one of them clears 4.5:1 against
     the four backgrounds this app paints text on, including accent-soft, which is the darkest
     and was where the old values failed worst.

     The old --faint was #989186: 2.92:1 on paper, and it carries most of the secondary
     information in the app. 157 pieces of text failed AA because of that one token. The new
     values keep three visibly separate tiers rather than collapsing faint into muted, because
     the hierarchy is doing real work. */
  --muted:   #54504a;   /* 7.5:1 on paper */
  --faint:   #6e675d;   /* 4.6:1 at worst */
  --line:    #e5ded2;
  --accent:  #8a4b2a;
  --accent-soft: #f4e6dc;
  --good:    #3f6b47;
  --warn:    #915d19;
  --shadow:  0 1px 2px rgba(28,26,23,.05), 0 8px 24px -12px rgba(28,26,23,.18);
  --radius:  14px;

  /* CH-139, 12 Oct: the app's own type is self-hosted too. These were system stacks, so a
     heading was Iowan Old Style on an iPhone, Palatino on Windows and Georgia elsewhere, and
     Christian saw the app change font from one browser to the next. */
  --serif: "EB Garamond", ui-serif, Georgia, serif;
  --sans:  "Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:   #141210;
    --surface: #1d1a16;
    --sunk:    #232019;
    --ink:     #f1ece4;
    /* Same exercise in the dark theme, which this suite had never rendered at all until now,
       so none of its pairings had ever been measured. */
    --muted:   #bab3a9;
    --faint:   #968e82;
    --line:    #302b24;
    --accent:  #d9945f;
    --accent-soft: #33251b;
    --good:    #7fb389;
    --warn:    #d8a860;
    --shadow:  0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.6);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

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

/* ---------------------------------------------------------------- chrome */

header.top {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: saturate(150%) blur(12px);
  border-bottom: 1px solid var(--line);
}

/* Back, left of the mark, on every page that is not a tab (14 Oct 2026). 44px to tap. */
.nav-back {
  flex: none; width: 36px; height: 36px; margin: -4px -4px -4px -8px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; border-radius: 999px; color: var(--ink);
}
.nav-back[hidden] { display: none; }
.nav-back svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 2.2;
  stroke-linecap: round; stroke-linejoin: round; }
.nav-back:hover, .nav-back:focus-visible { background: var(--accent-soft); }
.nav-back::after { content: ''; position: absolute; inset: -4px; }
.nav-back { position: relative; }

/* The wordmark is Poltawski Nowy Bold, as in the logo (14 Oct 2026). Only the header uses it. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: .26em;
  font-family: 'Poltawski Nowy', var(--serif);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -.005em;
  color: var(--ink);
  margin-right: auto;
}
.brand:hover { text-decoration: none; }
.brand .mark { color: #8a4b2a; display: inline-flex; }
.brand .mark-svg { height: 1.6em; width: auto; vertical-align: 0; }
@media (prefers-color-scheme: dark) { .brand .mark { color: var(--accent); } }
.mark-svg { display:inline-block; vertical-align:-0.05em; }

main { max-width: 780px; margin: 0 auto; padding: 20px 16px 96px; }

/* ---------------------------------------------------------------- type */

h1 { font-family: var(--serif); font-size: 28px; line-height: 1.2; letter-spacing: -.02em; margin: 0 0 6px; font-weight: 600; }
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .09em; color: var(--muted); margin: 32px 0 12px; font-weight: 600; }
h3 { font-family: var(--serif); font-size: 19px; margin: 0 0 4px; font-weight: 600; letter-spacing: -.01em; }
p  { margin: 0 0 12px; }
.lede { color: var(--muted); margin: 0 0 20px; }
.small { font-size: 13px; color: var(--muted); }
.tiny  { font-size: 12px; color: var(--faint); }

/* ---------------------------------------------------------------- controls */

button, .btn {
  font: inherit; font-size: 15px; font-weight: 500;
  border-radius: 10px; border: 1px solid var(--line);
  background: var(--surface); color: var(--ink);
  padding: 9px 14px; cursor: pointer;
  transition: background .12s, border-color .12s, transform .06s;
}
button:hover, .btn:hover { border-color: var(--accent); text-decoration: none; }
button:active { transform: translateY(1px); }
button:disabled { opacity: .45; cursor: not-allowed; }

button.primary, .btn.primary {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
@media (prefers-color-scheme: dark) { button.primary, .btn.primary { color: #1a1310; } }
button.ghost { background: transparent; border-color: transparent; color: var(--muted); padding: 6px 8px; }
button.ghost:hover { color: var(--accent); }
/* The small button, used everywhere. It measured 32px tall, which clears the WCAG 2.2 AA
   minimum of 24 and sits well under the 44 that both Apple and Google recommend. 36 with
   generous padding is the compromise that does not force every dense row to be respaced. */
button.tiny-btn, .btn.tiny-btn { font-size: 13px; padding: 7px 12px; border-radius: 8px;
  min-height: 36px; display: inline-flex; align-items: center; justify-content: center; }
button.danger { color: #a33; border-color: var(--line); }

input, textarea, select {
  font: inherit; width: 100%;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 10px;
  padding: 10px 12px;
}
input:focus, textarea:focus, select:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
textarea { resize: vertical; min-height: 84px; line-height: 1.5; }
label { display: block; font-size: 13px; font-weight: 600; color: var(--muted); margin: 0 0 5px; }
.field { margin-bottom: 14px; }
.row { display: flex; gap: 10px; align-items: center; }
.row.wrap { flex-wrap: wrap; }
.grow { flex: 1; min-width: 0; }

/* ---------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 12px;
  box-shadow: var(--shadow);
}
.card.flat { box-shadow: none; }
a.card { color: inherit; }
a.card:hover { text-decoration: none; border-color: var(--accent); }

.club-card { display: flex; gap: 14px; align-items: flex-start; }
.club-card .body { flex: 1; min-width: 0; }
.club-card h3 { margin-bottom: 2px; }

/* --------------------------------------------------------------- open thread */

/* Visibly a different kind of place from a chapter discussion: no lock, and it says so.
   The tinted head is the whole signal, so nobody has to wonder why this one is readable. */
.thread { border-color: var(--accent-soft); }
.thread-head {
  margin: -16px -16px 14px;
  padding: 12px 16px;
  background: var(--accent-soft);
  border-radius: var(--radius) var(--radius) 0 0;
}
.thread-head strong { display: block; font-size: 14px; margin-bottom: 3px; }
.thread-head .tiny { display: block; color: var(--muted); }
.thread-list { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 4px; }

/* ------------------------------------------------------------- chapter picker */

/* The whole progress line is the way in. Somebody who has just joined a club on chapter 30
   should not have to press a + button thirty times, and the number telling them where they
   are is the obvious thing to reach for. */
.posline {
  display: block; width: 100%;
  background: none; border: 0; padding: 0;
  color: inherit; font: inherit; text-align: left;
  cursor: pointer;
}
.posline .jump {
  float: right;
  font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--accent);
}
.posline .track { display: block; margin-top: 6px; }
.posline:hover .jump { text-decoration: underline; }
.posline:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }

/* A grid rather than a dropdown: the whole book is visible at once, where you are is marked,
   and every chapter is one tap from every other. Capped in height so a 59 chapter book does
   not turn the sheet into a page. */
.pos-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(46px, 1fr));
  gap: 6px;
  max-height: 42vh;
  overflow-y: auto;
  padding: 2px;
  -webkit-overflow-scrolling: touch;
}
.pos-cell {
  aspect-ratio: 1;
  display: grid; place-items: center;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  color: var(--muted);
  font-family: var(--sans); font-size: 15px; font-weight: 500;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.pos-cell:hover { border-color: var(--accent); color: var(--ink); }
.pos-cell.done { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
.pos-cell.here {
  background: var(--accent); border-color: var(--accent);
  color: var(--paper); font-weight: 600;
}
/* A stretch of the book, in the same two shades: the chapter at each end solid, everything
   between them soft, so a range reads as one thing rather than as two selected cells. */
.pos-cell.span { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
.pos-cell.span-end {
  background: var(--accent); border-color: var(--accent);
  color: var(--paper); font-weight: 600;
}

/* ---------------------------------------------------------------- banners */

/* A club's own picture, across the top of its page. Fixed height so the page below it does not
   jump around while the image loads, and object-fit: cover so any shape the admin uploads
   crops to the same band rather than stretching. */
.club-banner {
  height: 150px;
  margin: -2px 0 16px;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--sunk);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
}
.club-banner img { width: 100%; height: 100%; object-fit: cover; display: block; }

.banner-preview {
  height: 96px;
  margin-bottom: 9px;
  border-radius: 10px;
  overflow: hidden;
  background: var(--sunk);
  border: 1px solid var(--line);
  display: grid; place-items: center;
}
.banner-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Somebody invited but not yet answered. Not a .reader: there is no person behind it yet,
   only an address, and an address must never be truncated to fit a button beside it. */
.invited { padding: 10px 0; border-bottom: 1px solid var(--line); }
.invited:last-child { border-bottom: 0; padding-bottom: 0; }
.invited .addr { overflow-wrap: anywhere; margin-bottom: 6px; }

/* ---------------------------------------------------------------- covers */

.cover {
  width: 56px; height: 84px; flex: none;
  border-radius: 4px;
  background: var(--sunk) center/cover no-repeat;
  border: 1px solid var(--line);
  box-shadow: 2px 2px 0 color-mix(in srgb, var(--ink) 6%, transparent);
  display: grid; place-items: center;
  font-family: var(--serif); font-size: 20px; color: var(--faint);
  overflow: hidden;
}
.cover.lg { width: 92px; height: 138px; font-size: 28px; }
/* A club's own photo in a list. Square and round, like a reader's, rather than cropped into the
   portrait shape of a book spine. */
.cover.club-thumb { width: 56px; height: 56px; border-radius: 50%; box-shadow: none; }
/* The letter and the picture occupy the same cell, so a cover that fails to load can simply
   remove itself and the letter is already behind it. */
.cover > * { grid-area: 1 / 1; }
.cover img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---------------------------------------------------------------- badges */

.badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; font-weight: 600;
  padding: 3px 9px; border-radius: 999px;
  background: var(--sunk); color: var(--muted);
  border: 1px solid transparent;
}
.badge.accent { background: var(--accent-soft); color: var(--accent); }
.badge.live   { background: color-mix(in srgb, var(--good) 15%, transparent); color: var(--good); }
.badge.count  { background: var(--accent); color: #fff; }
.badge.pick {
  background: transparent;
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent);
  font-weight: 500;
}
.badge.pick::before { content: "★"; font-size: 10px; opacity: .8; }
/* A pick whose source is known is a link to it (improvement 17). It should not read as body-text
   link: no underline, and the badge's own colour, with the tint filling in on hover. */
a.badge.pick { text-decoration: none; }
a.badge.pick:hover, a.badge.pick:focus-visible {
  background: var(--accent-soft);
  border-color: var(--accent);
}
@media (prefers-color-scheme: dark) { .badge.count { color: #1a1310; } }

/* ---------------------------------------------------------------- chapters */

.chapters { display: flex; flex-direction: column; gap: 8px; }

.chapter {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 11px;
  background: var(--surface);
  cursor: pointer;
  text-align: left; width: 100%;
  font-size: 15px;
}
.chapter:hover { border-color: var(--accent); }
.chapter .num {
  font-family: var(--serif); font-size: 15px; font-weight: 600;
  width: 30px; flex: none; color: var(--muted); text-align: center;
}
.chapter .label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.chapter.locked {
  background: repeating-linear-gradient(
    -45deg, transparent, transparent 7px,
    color-mix(in srgb, var(--line) 45%, transparent) 7px,
    color-mix(in srgb, var(--line) 45%, transparent) 8px);
  border-style: dashed;
  color: var(--faint);
  cursor: default;
}
.chapter.locked:hover { border-color: var(--line); }
.chapter.locked .label { font-style: italic; }
.chapter.read .num { color: var(--good); }
.chapter.current { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

/* ---------------------------------------------------------------- progress */

.track { height: 6px; border-radius: 999px; background: var(--sunk); overflow: hidden; }
.track > i { display: block; height: 100%; background: var(--accent); border-radius: 999px; transition: width .3s; }

.reader { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--line); }
.reader:last-child { border-bottom: 0; }
.reader .who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reader .track { width: 96px; flex: none; }
.reader .pos { font-size: 13px; color: var(--muted); width: 62px; text-align: right; flex: none; font-variant-numeric: tabular-nums; }

.avatar {
  width: 28px; height: 28px; border-radius: 50%; flex: none;
  background: var(--accent-soft); color: var(--accent);
  display: grid; place-items: center;
  font-size: 12px; font-weight: 700;
  overflow: hidden;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar.sm { width: 24px; height: 24px; font-size: 11px; }

/* ---------------------------------------------------------------- range */

/* The distance slider measured 22px tall, under the 24 that WCAG 2.2 AA asks for and well
   under what a thumb wants on a map control. */
input[type="range"] { min-height: 28px; }
input[type="range"] {
  -webkit-appearance: none; appearance: none;
  background: transparent; border: 0; padding: 0;
  width: 100%; height: 22px; cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track {
  height: 5px; border-radius: 999px; background: var(--sunk); border: 1px solid var(--line);
}
input[type="range"]::-moz-range-track {
  height: 5px; border-radius: 999px; background: var(--sunk); border: 1px solid var(--line);
}
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 20px; height: 20px; margin-top: -8px;
  border-radius: 50%; background: var(--accent);
  border: 2px solid var(--surface);
  box-shadow: 0 1px 4px rgba(0,0,0,.25);
}
input[type="range"]::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--surface);
  box-shadow: 0 1px 4px rgba(0,0,0,.25);
}
input[type="range"]:focus { outline: none; }
input[type="range"]:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------------------------------------------------------------- quotes */

.cover.sm { width: 34px; height: 51px; font-size: 14px; border-radius: 3px; }

.quote-body {
  font-family: var(--serif);
  font-size: 19px;
  line-height: 1.45;
  margin: 0;
  padding: 0 0 0 16px;
  border-left: 3px solid var(--accent-soft);
  white-space: pre-wrap;
  word-wrap: break-word;
  transition: filter .18s, opacity .18s;
}
/* The blur was scoped to .quote-body.spoiler, which stopped working the moment a quote became
   a drawn page wrapped in a plain div. A spoiler that silently renders in the clear is the
   worst failure this app has, so the rule is on .spoiler itself now and applies to whatever it
   wraps. */
.spoiler {
  filter: blur(7px);
  opacity: .75;
  cursor: pointer;
  user-select: none;
  transition: filter .18s, opacity .18s;
}
.spoiler:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
.spoiler-note { margin-top: 10px; font-style: italic; }

.quote-card { padding: 18px 16px; }

/* ---------------------------------------------------------------- quote cards */

.qcard {
  position: relative; overflow: hidden;
  border-radius: 3px; padding: 30px 26px;
  min-height: 190px;
  display: flex; flex-direction: column; justify-content: center;
  box-shadow: 0 1px 2px rgba(0,0,0,.10), 0 14px 30px -18px rgba(0,0,0,.45);
}
.paper-clean  { background: #fdfcf9; color: #1c1917; }
.paper-cream  { background: #f7f0e1; color: #241f18; }
.paper-aged   { background: #eee0c4; color: #2b2318; }
.paper-yellow { background: #efe3bd; color: #33291a; }
.paper-coffee { background: #eadfc8; color: #2a2117; }
.paper-dark   { background: #241f1b; color: #efe6d6; }

.q-grain  { position: absolute; inset: 0; pointer-events: none; mix-blend-mode: multiply;
            filter: url(#q-grain); opacity: .5; }
.q-fox    { position: absolute; inset: 0; pointer-events: none; filter: url(#q-fox); opacity: .55; }
.q-burn   { position: absolute; inset: 0; pointer-events: none;
            box-shadow: inset 0 0 44px rgba(120,92,48,.30), inset 0 0 10px rgba(90,66,30,.20); }
.q-coffee {
  position: absolute; width: 46%; aspect-ratio: 1; right: -8%; top: 4%;
  pointer-events: none; border-radius: 50%; transform: rotate(-12deg);
  background: radial-gradient(circle, transparent 0 58%, rgba(140,96,44,.20) 59% 68%,
              rgba(140,96,44,.09) 69% 78%, transparent 79%);
}
.q-marks { position: absolute; inset: 0; pointer-events: none; }
.q-marks.under { z-index: 1; }
.q-marks.over  { z-index: 3; }

.q-passage {
  position: relative; z-index: 2; margin: 0;
  font-size: 18px; line-height: 1.6;
}
.q-attrib {
  position: relative; z-index: 2; margin-top: 22px;
  font-size: 10px; letter-spacing: .07em; text-transform: uppercase; opacity: .6;
}
.q-w.pick { cursor: pointer; border-radius: 3px; }
/* Sideways is the mark, up and down is the sheet scrolling. See wireDraftDrag. */
.qcard [data-w] { -webkit-user-select: none; user-select: none; }
#qc-draft .q-passage { touch-action: pan-y; }
.q-w.pick:hover { background: color-mix(in srgb, currentColor 12%, transparent); }
.q-w.picking { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 3px; }

.swatches { display: flex; flex-wrap: wrap; gap: 8px; }
.swatch {
  min-width: 44px; height: 40px; padding: 0 10px;
  border: 1px solid var(--line); border-radius: 9px;
  font-size: 13px; cursor: pointer;
}
.swatch.wide { min-width: 62px; }
.swatch.dot { width: 32px; min-width: 32px; height: 32px; border-radius: 50%; padding: 0; }
.swatch.on { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }

/* ---------------------------------------------------------------- comments */

.comment { padding: 12px 0; border-bottom: 1px solid var(--line); }
.comment:last-child { border-bottom: 0; }
.comment .head { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.comment .name { font-weight: 600; font-size: 14px; }
.comment .body { white-space: pre-wrap; word-wrap: break-word; }
/* The comment a reply notification sent you to. It has to be findable in a long thread without
   being shouted at, so: the accent tint for a couple of seconds, then it fades back to the page.
   A transition rather than an animation, so there is nothing for prefers-reduced-motion to stop. */
.comment.lit {
  background: var(--accent-soft);
  box-shadow: inset 3px 0 0 var(--accent);
  border-radius: 8px;
  padding-left: 12px;
  padding-right: 12px;
  transition: background .8s ease, box-shadow .8s ease;
}

.note-box { background: var(--sunk); border-radius: 11px; padding: 14px; }

/* ---------------------------------------------------------------- misc */

.empty { text-align: center; padding: 40px 20px; color: var(--muted); }
.empty .mark { font-family: var(--serif); font-size: 40px; display: block; margin-bottom: 8px; color: var(--faint); }

/* Scoped to .tabs on purpose. The floating navigation pill at the bottom of the screen also
   uses .tab, its rules come later in this file, and at equal specificity later wins. Two
   components sharing a class name is the bug; scoping both is the fix. */
.tabs {
  display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin-bottom: 16px;
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.tabs::-webkit-scrollbar { display: none; }
.tabs .tab {
  flex: 0 0 auto; white-space: nowrap;
  border: 0; background: none; padding: 9px 12px; border-radius: 0;
  color: var(--muted); font-weight: 600; font-size: 14px; font-family: var(--sans);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  cursor: pointer;
}
.tabs .tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
.tabs .tab:hover { border-color: transparent; border-bottom-color: var(--line); }
.tabs .tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Some tab rows are buttons and some are links. A link that underlines on hover in a row of
   buttons reads as a different kind of control than the one beside it. */
a.tab:hover, a.tab:focus-visible { text-decoration: none; }

/* A join request is somebody waiting on you. Behind a tab it would go unseen, so the tab
   carries the count. */
.tabs .tab-count {
  display: inline-grid; place-items: center;
  min-width: 18px; height: 18px; margin-left: 6px; padding: 0 5px;
  border-radius: 999px;
  background: var(--accent); color: var(--paper);
  font-size: 11px; font-weight: 700;
  vertical-align: 1px;
}
/* The first heading in a pane sits under the tabs, not under 32px of nothing. */
.tabpane > h2:first-child { margin-top: 10px; }

/* A sheet is the whole interaction while it is open, so the page behind it holds still.

   Three separate things were letting the page move, and only fixing all three fixes it:

   1. vh on a phone is the viewport with the browser chrome hidden, so 88vh is taller than
      what you can see. The sheet then fits inside its own max-height, has nothing to scroll,
      and the finger scrolls the document instead. dvh is the height that is actually there.
   2. Scroll chaining: reach the end of the sheet and the browser keeps going into whatever
      is underneath. overscroll-behavior: contain stops at the edge.
   3. Nothing pinned the body. Safari on iOS ignores overflow:hidden there for touch, so the
      lock is position:fixed with the scroll offset held in top, put on by sheet() and taken
      off with the offset restored by closeSheet(). */
.sheet-backdrop {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(20,18,16,.45);
  display: grid; place-items: end center;
  padding: 0;
  overscroll-behavior: contain;
}
.sheet {
  background: var(--surface); width: 100%; max-width: 560px;
  border-radius: 18px 18px 0 0; padding: 20px 18px 28px;
  max-height: 88vh; overflow-y: auto;
  overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line); border-bottom: 0;
}
@supports (max-height: 88dvh) { .sheet { max-height: 88dvh; } }
/* A searching sheet holds its height so a list arriving does not shove the page up. */
.sheet.tall { height: 88vh; }
@supports (height: 88dvh) { .sheet.tall { height: 88dvh; } }
/* A sheet you search in, which so far is the GIF picker. Ryan, 2 October: "Can this go up all
   the way?" On a phone the keyboard takes half the screen, so a sheet that stops at 88% of it
   shows two and a half rows of GIFs, and choosing between two and a half rows is not choosing.

   The height subtracts `--kb` rather than trusting the unit, because the two cases differ: in a
   Safari tab the layout viewport does not move when the keyboard opens, so 100dvh overshoots by
   the height of the keyboard and `--kb` is what takes it back; installed to the home screen iOS
   shrinks the layout viewport itself, 100dvh is already right, and `--kb` measures as nothing.
   One expression, correct in both.

   The sheet stops scrolling and the grid inside it scrolls instead, so the search box and the
   GIPHY credit stay put while the results move under them. That makes the close button a flex
   item rather than a float, which is why it is pinned to the corner here. */
.sheet.full {
  position: relative;
  height: calc(100vh - var(--kb, 0px));
  max-height: none;
  overflow: hidden;
  display: flex; flex-direction: column;
  padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
}
@supports (height: 100dvh) { .sheet.full { height: calc(100dvh - var(--kb, 0px)); } }
.sheet.full > * { flex: 0 0 auto; }
.sheet.full .sheet-x {
  position: absolute; top: 10px; right: 10px; float: none; margin: 0;
}
.sheet.full .gif-grid { flex: 1 1 auto; min-height: 0; max-height: none; }

body.sheet-open { position: fixed; left: 0; right: 0; width: 100%; overflow: hidden; }

/* Every sheet's own way out. Floated so the heading wraps around it rather than being pushed
   down a line, sticky so scrolling a long sheet never takes it off the screen, and 44px square
   because that is the smallest thing a thumb reliably hits. If sticky ever fails it degrades to
   a plain floated button in the corner, which is still a way out. */
.sheet-x {
  float: right; position: sticky; top: -6px; z-index: 3;
  width: 44px; height: 44px; margin: -12px -12px 0 8px; padding: 0;
  border: 0; border-radius: 999px; background: none; color: var(--muted);
  display: grid; place-items: center; cursor: pointer; line-height: 0;
}
.sheet-x svg { width: 20px; height: 20px; }
.sheet-x:hover { background: var(--sunk); color: var(--ink); }
.sheet-x:active { background: var(--line); }
@media (min-width: 640px) {
  .sheet-backdrop { place-items: center; padding: 24px; }
  .sheet { border-radius: 18px; border-bottom: 1px solid var(--line); }
  @supports (max-height: 88dvh) { .sheet { max-height: calc(100dvh - 48px); } }
  @supports (height: 88dvh) { .sheet.tall { height: min(680px, calc(100dvh - 48px)); } }
  /* On a desktop there is no keyboard eating the window, so the full-screen picker becomes an
     ordinary tall dialog rather than a sheet stretched over a 1400px-high browser. */
  .sheet.full { height: min(680px, calc(100vh - 48px)); }
  @supports (height: 100dvh) { .sheet.full { height: min(680px, calc(100dvh - 48px)); } }
}

/* Ryan, 16 September: "The done button doesn't work here." It did. A toast is fixed at the
   bottom of the screen at z-index 90, above the sheet, and a sheet is anchored to the bottom
   too, so for the 2.6 seconds a toast is up it sits exactly on top of whatever button the sheet
   ends with and took the tap instead. Nothing was wrong with the button or the handler, which is
   why it read as random: wait two seconds and it worked.

   A toast is something the app says, never something you press. The confetti layer next to it
   got this right from the start; this one did not. */
/* A rounded rectangle, not a pill. A pill radius on a message that wraps to five lines is a
   circle, which is what Christian's screenshot showed: a black disc over the Save button. */
.toast {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: 90; background: var(--ink); color: var(--paper);
  padding: 11px 16px; border-radius: 14px; font-size: 14px; font-weight: 500; line-height: 1.4;
  box-shadow: var(--shadow); width: max-content; max-width: min(90vw, 420px);
  text-wrap: balance; pointer-events: none;
}

.result {
  display: flex; gap: 12px; align-items: center; width: 100%;
  padding: 10px; border-radius: 10px; border: 1px solid transparent;
  background: none; text-align: left; cursor: pointer;
}
.result:hover { background: var(--sunk); border-color: var(--line); }
/* The arrow keys walk the list without moving focus off the text box, so the row the reader is on
   has to be visible without a focus ring to show it. CH-58. */
.result[aria-selected="true"] { background: var(--sunk); border-color: var(--accent); }

hr.rule { border: 0; border-top: 1px solid var(--line); margin: 24px 0; }

.spinner {
  width: 18px; height: 18px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--accent);
  animation: spin .7s linear infinite; display: inline-block; vertical-align: -3px;
}
@keyframes spin { to { transform: rotate(360deg); } }

.avatar.xl { width: 76px; height: 76px; font-size: 28px; flex: none; }

.badge.warn { background: color-mix(in srgb, var(--warn) 16%, transparent); color: var(--warn); }

/* ---------------------------------------------------------------- tab bar */
/* A floating pill, not a bar welded to the bottom of the screen. Content runs underneath it
   and shows through, which is the whole point: the blur has to have something to blur.

   The glass is four things stacked, and it falls apart if you drop any of them:
     1. a translucent fill, so the page is visible through it
     2. backdrop-filter blur plus saturation, so what shows through is soft and colour stays alive
     3. a hairline border that is lighter at the top than the bottom, which is what reads as a
        curved edge catching light
     4. an inset highlight along the top inner edge, the specular line down the middle of the
        curve, and a real shadow underneath so it sits above the page rather than in it */

/* translateZ(0) and will-change are not decoration. Ryan, 30 September, on a real iPhone: the bar
   stopped following the viewport while scrolling one way and "stays there until I scroll the
   opposite direction and it comes back down". That is a stale paint, not a positioning error --
   measured in Chromium at iPhone size, the bar holds exactly 10px off the bottom at every scroll
   position including the end of a long page. It is WebKit-only, and the combination that provokes
   it is a fixed element carrying a backdrop-filter: iOS has to re-sample what is behind the glass
   every frame, and defers it. Giving the bar its own compositing layer means the system moves the
   layer instead of repainting it.

   This CANNOT be verified from here. The suite runs Chromium, there is no WebKit in the container
   and it cannot be downloaded, so Ryan's phone is the only instrument. If this does not hold, the
   next thing to try is moving the blur onto a child of the bar rather than the bar itself, and the
   thing after that is giving up the glass for an opaque background. */
#tabbar {
  position: fixed; z-index: 50;
  left: 50%; transform: translateX(-50%) translateZ(0);
  will-change: transform;
  bottom: calc(10px + env(safe-area-inset-bottom, 0px));
  width: min(420px, calc(100% - 24px));
  display: flex; align-items: center; justify-content: space-around;
  gap: 2px;
  padding: 6px;
  border-radius: 999px;

  background: color-mix(in srgb, var(--surface) 72%, transparent);
  backdrop-filter: blur(22px) saturate(180%);
  -webkit-backdrop-filter: blur(22px) saturate(180%);

  border: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  border-top-color: color-mix(in srgb, #fff 55%, transparent);

  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 45%, transparent),
    0 1px 2px rgba(28, 26, 23, .06),
    0 12px 32px -8px rgba(28, 26, 23, .28);
}

@media (prefers-color-scheme: dark) {
  #tabbar {
    background: color-mix(in srgb, var(--surface) 62%, transparent);
    border-top-color: color-mix(in srgb, #fff 16%, transparent);
    box-shadow:
      inset 0 1px 0 color-mix(in srgb, #fff 12%, transparent),
      0 12px 32px -8px rgba(0, 0, 0, .7);
  }
}

#tabbar:empty { display: none; }

/* The page has to clear the pill and the gap under it, or the last card sits behind glass. */
body { padding-bottom: calc(84px + env(safe-area-inset-bottom, 0px)); }

#tabbar .tab {
  position: relative;
  flex: 0 1 auto;
  display: flex; align-items: center; justify-content: center;
  gap: 0;
  height: 44px; min-width: 48px;
  padding: 0 13px;
  border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 12.5px; font-weight: 600; letter-spacing: .01em;
  color: var(--faint);
  text-decoration: none;
  border-radius: 999px;
  -webkit-tap-highlight-color: transparent;
  transition: color .22s, background .22s, padding .22s cubic-bezier(.2,.8,.2,1);
}

#tabbar .tab svg {
  width: 22px; height: 22px;
  fill: none;
  stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  flex: 0 0 auto;
}

/* Labels are hidden until a tab is the one you are on. Five words in a pill is clutter; one
   word tells you where you are, and the width animating open is the liquid part. */
/* :not(.add-ring), because the add button's disc is a span too and this rule collapsed it to
   nothing. The + was invisible on every screen and the 44px button box around it still
   measured correctly, which is why the test caught none of it and the screenshot did. */
#tabbar .tab > span:not(.add-ring) {
  max-width: 0; overflow: hidden; white-space: nowrap; opacity: 0;
  transition: max-width .26s cubic-bezier(.2,.8,.2,1), opacity .18s, margin .26s;
}

#tabbar .tab.on {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 40%, transparent);
}
#tabbar .tab.on svg { stroke-width: 2; }
#tabbar .tab.on > span:not(.add-ring) { max-width: 90px; opacity: 1; margin-left: 7px; }

#tabbar .tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The only badge in the app. It means somebody wants you, never that you are behind. */
#tabbar .tab .dot {
  position: absolute; top: 5px; right: 8px;
  min-width: 8px; height: 8px; padding: 0;
  border-radius: 999px;
  background: var(--accent);
  font-size: 0; line-height: 0;
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--surface) 80%, transparent);
}

/* The verb in the middle. It is a filled disc rather than a line drawing because it is the
   only thing in the bar that does something, and it should not have to be read to be found:
   the thumb goes to the middle and the middle is the one that is solid.

   Its label stays hidden even when pressed, since there is no "on" state for a button that
   opens a sheet and closes again. The accessible name carries it. */
#tabbar .tab.add { padding: 0 6px; }
#tabbar .tab.add .add-ring {
  display: grid; place-items: center;
  width: 40px; height: 40px; border-radius: 999px;
  background: var(--accent); color: var(--paper);
  box-shadow: 0 1px 2px rgba(28,26,23,.18), 0 6px 14px -4px color-mix(in srgb, var(--accent) 55%, transparent);
  transition: transform .16s cubic-bezier(.2,.8,.2,1), box-shadow .16s;
}
#tabbar .tab.add svg { width: 21px; height: 21px; stroke-width: 2.2; }
#tabbar .tab.add:hover .add-ring { transform: scale(1.06); }
#tabbar .tab.add:active .add-ring { transform: scale(.94); box-shadow: 0 1px 2px rgba(28,26,23,.2); }
#tabbar .tab.add > span:not(.add-ring) { display: none; }
@media (prefers-color-scheme: dark) {
  #tabbar .tab.add .add-ring { color: #1a1010; }
}

@media (prefers-reduced-motion: reduce) {
  .tab, .tab span, #tabbar .tab.add .add-ring { transition: none; }
}

/* --------------------------------------------------------------- icon buttons */
/* A destructive action should be reachable and quiet. Delete used to be the loudest word on
   the feed, repeated once per quote you had written. */

.icon-btn {
  display: grid; place-items: center;
  width: 32px; height: 32px; padding: 0;
  border: 0; border-radius: 8px;
  background: none; color: var(--faint);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.icon-btn:hover, .icon-btn:focus-visible { background: var(--sunk); color: var(--ink); }
.icon-btn svg { width: 18px; height: 18px; }

.icon-btn svg, svg.mini {
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
svg.mini { width: 14px; height: 14px; flex: 0 0 auto; }

/* --------------------------------------------------------------- page header */
/* Title and its one action on the same line. A heading with a button stacked under it burns
   the most valuable strip of a phone screen restating what the heading already said. */

.page-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  margin-bottom: 18px;
}
.page-head h1 { margin: 0; }
.page-head > .primary, .page-head > .btn { flex: 0 0 auto; }

/* Required by ODbL wherever OpenStreetMap data is shown. */
.osm-credit { margin: 8px 0 0; color: var(--faint); }

/* Reading numbers on a profile. Counts of what somebody has done, nothing about pace. */
.numbers { display:flex; flex-wrap:wrap; gap:8px 16px; margin-top:14px;
  padding-top:14px; border-top:1px solid var(--line); }
.numbers span { font-size:13px; color:var(--muted); }
.numbers b { font-size:15px; color:var(--ink); font-weight:650; }

/* ---------------------------------------------------------------- the shelf

   A shelf, not a grid of thumbnails. Books of slightly different heights standing on a plank,
   with a wall behind them, and three ways to look at the same books: face-out covers, spines
   packed side-on, or a plain list. The finish is the person's own choice and is stored on
   their device.

   The plank is a repeating background on the container rather than a border on each book, so
   it runs the full width of the shelf even where the last row is half empty. That only works
   because every row is exactly the same height, which is why the book box is a fixed height
   and the book inside it varies instead. */
.shelf-unit {
  --bh: 106px;            /* the tallest a book stands */
  --plank: 12px;
  --row: calc(var(--bh) + var(--plank));
  --wall-1: #efe6d8; --wall-2: #e6dbc9;
  --wood-1: #8a5a34; --wood-2: #6b4225; --wood-edge: #4e2f19;
  border-radius: 10px;
  overflow: hidden;
  background: linear-gradient(180deg, var(--wall-1), var(--wall-2));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.5), inset 0 0 0 1px rgba(0,0,0,.06);
}
.shelf-unit[data-finish="oak"]     { --wood-1:#c99a5e; --wood-2:#a9793f; --wood-edge:#8a5f2e; }
.shelf-unit[data-finish="painted"] { --wood-1:#fbfaf7; --wood-2:#e6e0d5; --wood-edge:#c9c0b1;
                                     --wall-1:#f6f2ea; --wall-2:#ece5d9; }
.shelf-unit[data-finish="midnight"]{ --wood-1:#3a4150; --wood-2:#242a36; --wood-edge:#161a22;
                                     --wall-1:#2b303a; --wall-2:#1e222a; }

/* The plank is a repeating background, and its period has to be exactly the height of a flex
   line or it drifts a few pixels further out of true on every row. That is why every book box
   is exactly --row tall and the variation happens inside it: a shelf whose planks slide down
   the wall as you scroll is worse than no planks at all. */
.shelf-inner {
  display: flex; flex-wrap: wrap; align-content: flex-start;
  gap: 0; padding: var(--pad, 10px) 10px 0;
  background-image: repeating-linear-gradient(to bottom,
    transparent 0 var(--bh),
    var(--wood-1) var(--bh) calc(var(--bh) + var(--plank) * .45),
    var(--wood-2) calc(var(--bh) + var(--plank) * .45) calc(var(--bh) + var(--plank) * .82),
    var(--wood-edge) calc(var(--bh) + var(--plank) * .82) var(--row));
  background-position: 0 var(--pad, 10px);
  /* Depth: the wall is darker where it meets the top of the unit and at the edges. */
  box-shadow: inset 0 8px 12px -10px rgba(0,0,0,.35), inset 0 0 30px -14px rgba(0,0,0,.4);
}

.shelf-covers .shelf-book {
  height: var(--row); flex: 0 0 auto;
  display: flex; align-items: flex-end; justify-content: center;
  padding: 0 4px var(--plank); text-decoration: none;
  transition: transform .15s;
}
.shelf-covers .shelf-book .cover {
  height: calc(var(--bh) * var(--f, 1)); width: calc(var(--bh) * var(--f, 1) * .66);
  aspect-ratio: auto; border-radius: 1px 3px 3px 1px;
  box-shadow: 1px 0 0 rgba(255,255,255,.25) inset, 0 3px 5px rgba(0,0,0,.28);
}
.shelf-covers .shelf-book .cover img { border-radius: 1px 3px 3px 1px; }
.shelf-book.lean { transform: rotate(-2.5deg); transform-origin: bottom left; }

/* A book read with a club gets a bookmark ribbon down the cover. The covers view has no room
   for a caption without stopping being a shelf, and "read with the Thursday club" is the one
   fact about a finished book worth keeping visible. A key under the shelf says what it means,
   so it is a marking rather than a decoration. */
.shelf-covers .shelf-book .cover { position: relative; }
.shelf-covers .shelf-book.club .cover::after {
  content: ''; position: absolute; top: 0; left: 22%; width: 5px; height: 40%;
  background: var(--accent);
  clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 76%, 0 100%);
  box-shadow: 0 0 0 1px rgba(0,0,0,.12); }
.shelf-spines .spine.club::after {
  content: ''; position: absolute; left: 0; right: 0; height: 5px; z-index: 1;
  top: calc(var(--bh) * (1 - var(--t)));
  background: var(--accent); }
.shelf-legend { margin: 8px 2px 0; display: flex; align-items: center; gap: 6px; }
.ribbon-key { width: 5px; height: 12px; flex: none; background: var(--accent);
  clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 74%, 0 100%); }
.shelf-covers .shelf-book:hover, .shelf-covers .shelf-book:focus-visible {
  transform: translateY(-4px) rotate(0deg); }

/* Spines. Thickness, height and colour all come off the same per-book seed, so a shelf of
   fifty looks like fifty books rather than fifty bars of a chart. */
.shelf-spines { --bh: 132px; --pad: 14px; }
/* Every spine's box is the full row height; --t is how much of it the book actually fills, so
   the row stays true while the books differ. */
.shelf-spines .spine {
  flex: 0 0 auto; height: var(--bh); margin-bottom: var(--plank);
  display: flex; align-items: flex-end; justify-content: center;
  text-decoration: none; transition: transform .15s; position: relative;
}
.shelf-spines .spine {
  background-color: hsl(var(--sh) 26% var(--sl));
  background-image: linear-gradient(90deg,
    hsl(var(--sh) 24% calc(var(--sl) - 8%)) 0 3px,
    transparent 3px calc(100% - 3px),
    hsl(var(--sh) 24% calc(var(--sl) - 10%)) calc(100% - 3px) 100%);
  background-repeat: no-repeat;
  background-position: bottom;
  background-size: 100% calc(var(--bh) * var(--t));
  border-radius: 2px 2px 0 0;
}
.shelf-spines .spine::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: calc(var(--bh) * var(--t));
  border-radius: 2px 2px 0 0;
  box-shadow: 0 3px 5px rgba(0,0,0,.3);
}
.spine-text {
  position: relative; z-index: 1; margin-bottom: 8px;
  writing-mode: vertical-rl; transform: rotate(180deg);
  font-family: "EB Garamond", Georgia, serif; font-size: 11px; letter-spacing: .3px;
  color: rgba(255,255,255,.94); white-space: nowrap; overflow: hidden;
  max-height: calc(var(--bh) * var(--t) - 16px); text-overflow: ellipsis;
}
.shelf-spines .spine:hover, .shelf-spines .spine:focus-visible { transform: translateY(-5px); }

/* The plain view, for when somebody wants to read the shelf rather than look at it. */
.shelf-list .shelf-row {
  display: flex; gap: 12px; align-items: center; padding: 9px 0;
  border-bottom: 1px solid var(--line); text-decoration: none; color: inherit; }
.shelf-list .shelf-row:last-child { border-bottom: 0; }
.shelf-list .shelf-row .cover { width: 34px; height: 51px; flex: none; }
.shelf-list .tiny.dim { color: var(--faint); }

.shelf-style { margin: 10px 0 14px; padding: 12px; border-radius: 8px; background: var(--sunk); }
.tiny-btn.swatch::before {
  content: ''; display: inline-block; width: 10px; height: 10px; margin-right: 5px;
  border-radius: 2px; vertical-align: -1px; border: 1px solid rgba(0,0,0,.2); }
.sw-walnut::before   { background: #7a4a29; }
.sw-oak::before      { background: #c1904f; }
.sw-painted::before  { background: #f2ede4; }
.sw-midnight::before { background: #2c323e; }

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

/* Buy links. Visibly a link to the shop it names: nothing here is proxied or shortened. */
.buy { display:flex; flex-wrap:wrap; gap:4px 6px; align-items:center; margin:0 0 14px; color:var(--muted); }
.buy a { color:inherit; text-decoration:underline; text-underline-offset:2px; min-height:24px; display:inline-flex; align-items:center; }
.buy .buy-note { flex-basis:100%; }

.rank { position:absolute; top:8px; left:8px; z-index:1;
  min-width:20px; height:20px; padding:0 5px; border-radius:10px;
  background:var(--ink); color:var(--paper);
  font-size:12px; font-weight:650; line-height:20px; text-align:center; }
#top-read .club-card { position:relative; }


/* Reading now: a club book and one of your own sit in the same list. */
#readers .club-card, .card > .club-card { margin-bottom: 14px; }
.card > .club-card:last-child { margin-bottom: 0; }

/* Admin panel. Dense on purpose: it is a work surface, not a page to be read. */
.adm-tiles { display:grid; grid-template-columns:repeat(auto-fill, minmax(108px, 1fr)); gap:10px; margin-bottom:18px; }
.adm-tile { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  padding:12px 14px; display:flex; flex-direction:column; gap:2px; }
.adm-tile b { font-size:22px; font-weight:650; line-height:1.1; }
.adm-tile span { font-size:12px; color:var(--muted); }
.adm-tile.warn b { color:var(--warn); }

.adm-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--radius);
  padding:12px 14px; margin-bottom:8px; }
.adm-row.off { opacity:.55; }
.adm-row .tiny { margin:2px 0 0; }
.adm-row strong { font-size:15px; }


/* The admin panel is a work surface, not something to be read, so it takes the width a desktop
   offers. Everything else stays a readable column: 780px is a deliberate choice for prose and
   book covers, and widening it would make the reading pages worse to serve the one page that
   is not reading. */
@media (min-width: 900px) {
  main.wide { max-width: 1180px; }
  main.wide .adm-tiles { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
  main.wide .adm-row { padding: 14px 16px; }
}

/* A book title is a way through to the book itself, wherever it appears. */
h3 > a { color: inherit; text-decoration: none; }
h3 > a:hover { text-decoration: underline; }
a.shelf-book { text-decoration: none; color: inherit; }


/* ------------------------------------------------------------------ reactions

   Illustrated faces drawn for Chapter (21 September), about twice the old size. A face nobody has
   used yet is faded, so a comment with one reaction reads as one reaction rather than four
   buttons; a used one is in full colour. Tapping one plays its own animation: the heart beats,
   the laugh shakes, the sad face droops and a tear falls, the mind-blown face bursts. */
.reacts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; align-items: center; }
.react {
  position: relative;
  display: inline-flex; align-items: center; gap: 5px;
  min-height: 44px; padding: 4px 10px 4px 6px; border-radius: 999px;
  border: 1px solid transparent; background: transparent; color: var(--muted);
  font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background .15s, color .15s, border-color .15s; }
.react .face { width: 30px; height: 30px; display: block; overflow: visible;
  transform-origin: 50% 60%; transition: filter .2s, transform .15s; }
.react .face * { transform-box: fill-box; }
.react.quiet .face { filter: saturate(.2) opacity(.62); }
.react:hover .face { filter: none; }
.react:hover { border-color: var(--line); }
.react.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.react:active .face { transform: scale(.9); }
.react .n { font-weight: 600; }
.react.pop { z-index: 2; }
.react.pop.k-heart .face { animation: react-beat .95s cubic-bezier(.3, .7, .4, 1); }
.react.pop.k-laugh .face { animation: react-laugh .9s ease-out; }
.react.pop.k-laugh .joy { animation: react-joy .9s ease-out; }
.react.pop.k-cry .face { animation: react-droop 1s ease-out; }
.react.pop.k-cry .tear { animation: react-fall 1s ease-in; }
.react.pop.k-mind .face { animation: react-blow .95s cubic-bezier(.2, .8, .3, 1); }
.react.pop.k-mind .burst { transform-origin: 50% 100%; animation: react-burst .95s cubic-bezier(.2, .8, .3, 1); }
@keyframes react-beat { 0% { transform: scale(1); } 14% { transform: scale(1.55); } 28% { transform: scale(1.15); }
  44% { transform: scale(1.5); } 70%, 100% { transform: scale(1); } }
@keyframes react-laugh { 0% { transform: scale(1) rotate(0); } 12% { transform: scale(1.5) rotate(-14deg); }
  26% { transform: scale(1.5) rotate(12deg); } 40% { transform: scale(1.45) rotate(-10deg); }
  54% { transform: scale(1.4) rotate(8deg); } 70% { transform: scale(1.2) rotate(-4deg); } 100% { transform: scale(1) rotate(0); } }
@keyframes react-joy { 0%, 15% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(6px); opacity: .2; } }
@keyframes react-droop { 0% { transform: scale(1); } 18% { transform: scale(1.45) translateY(-2px); }
  45% { transform: scale(1.42, 1.3) translateY(3px); } 75% { transform: scale(1.2, 1.15) translateY(2px); } 100% { transform: scale(1); } }
@keyframes react-fall { 0%, 20% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(16px); opacity: 0; } }
@keyframes react-blow { 0% { transform: scale(1); } 18% { transform: scale(1.55) translateY(-3px); } 40% { transform: scale(1.3); } 100% { transform: scale(1); } }
@keyframes react-burst { 0% { transform: scale(.3); opacity: 0; } 25% { transform: scale(1.4); opacity: 1; } 55% { transform: scale(1.1); } 100% { transform: scale(1); } }
/* !important on purpose: the animations above are keyed by kind, so they out-rank a plain
   ".react.pop .face" and the first version of this rule lost to them. A test caught it. */
@media (prefers-reduced-motion: reduce) {
  .react.pop .face, .react.pop .tear, .react.pop .joy, .react.pop .burst { animation: none !important; }
}

/* Readers of a book, grouped by where they are in it. */
.reader-chips { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.reader-chip { display: flex; align-items: center; gap: 8px; text-decoration: none; color: inherit;
  min-width: 0; }
.track.slim { width: 46px; height: 4px; flex: none; }

/* On the club board, the people you can talk to today. */
.reader.me .who { font-weight: 650; }
.reader.alongside { background: var(--accent-soft); border-radius: 6px;
  padding-left: 8px; padding-right: 8px; margin: 0 -8px; }
.reader .with { font-size: 11px; color: var(--accent); flex: none; }

/* Where a comment lands, under the box you are typing into. Quiet enough not to compete with
   the text you are writing, close enough to the Post button to be the last thing read. */
.destination {
  display: flex; gap: 6px; align-items: flex-start;
  margin: 8px 2px 0; font-size: 12px; line-height: 1.45; color: var(--muted);
}
.destination .mini { margin-top: 2px; color: var(--faint); }

/* ------------------------------------------------------------------- ratings
   Rate the book, never the reader. Nothing here appears on a profile. */
.rating { margin: 0 0 18px; padding: 12px 14px; border-radius: 10px; background: var(--sunk); }
.star { width: 17px; height: 17px; flex: none; fill: none; stroke: currentColor;
  stroke-width: 1.5; stroke-linejoin: round; }
.star-row { display: inline-flex; gap: 2px; }
.stars { position: relative; display: inline-block; line-height: 0; }
.star-row.dim { color: var(--faint); }
.star-row.lit { color: var(--accent); }
.star-row.lit .star { fill: currentColor; fill-opacity: .9; }
.star-lit { position: absolute; inset: 0; overflow: hidden; line-height: 0; }
.rating strong { font-size: 17px; font-variant-numeric: tabular-nums; }
.star-pick { display: inline-flex; gap: 2px; }
.star-btn { min-height: 44px; min-width: 32px; padding: 6px; border: 0; background: transparent;
  color: var(--faint); cursor: pointer; line-height: 0; }
.star-btn.on { color: var(--accent); }
.star-btn.on .star { fill: currentColor; fill-opacity: .9; }
.star-pick:hover .star-btn { color: var(--accent); }
.star-pick .star-btn:hover ~ .star-btn { color: var(--faint); }

/* --------------------------------------------------------------- date polls
   A meetup with no date yet is a question, and it renders as one. The bar is how many of the
   club could do that night, never a countdown or a deadline. */
.poll { margin: 4px 0 10px; }
.poll-list { display: flex; flex-direction: column; gap: 8px; }
.poll-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.poll-pick { position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 8px; flex: 1 1 200px; min-width: 0;
  min-height: 38px; padding: 7px 10px 9px; border-radius: 9px; text-align: left;
  border: 1px solid var(--line); background: transparent; color: inherit; cursor: pointer;
  font-size: 14px; }
.poll-pick:hover { border-color: var(--accent); }
.poll-row.mine .poll-pick { border-color: var(--accent); background: var(--accent-soft); }
.poll-pick .tick { width: 15px; flex: none; color: var(--accent); font-weight: 700; }
/* Wraps rather than trailing off. On a phone the admin's Lock this in sits beside each night and
   the ellipsis ate the time, which is the half of the option people are choosing between. */
.poll-pick .grow { min-width: 0; overflow-wrap: anywhere; }
/* The bar lives inside the option rather than under it, so a night is one thing to look at
   rather than two. It is how many of the club could make it, never a countdown. */
.poll-bar { position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: var(--sunk); overflow: hidden; }
.poll-bar > i { display: block; height: 100%; background: var(--accent); border-radius: 999px;
  transition: width .3s; }
.m-opt .field label { font-size: 12px; }

/* ------------------------------------------------------------------ wrap-up
   What a club did together, at the moment they finished. Never what they failed to do: no
   percentage of members who did not finish, nothing red, nothing anybody has to explain. */
.wrapup { border: 1px solid var(--accent); background: var(--accent-soft); }
.wrapup h2, .wrapup h3 { text-transform: none; letter-spacing: 0; font-size: 17px; }
.wrapup h2 { font-size: 21px; }
.wrapup .rating { background: color-mix(in srgb, var(--paper) 55%, transparent); }
.numbers-row { gap: 8px 18px; }
.numbers-row span { font-size: 13px; color: var(--muted); }
.numbers-row b { font-size: 17px; color: var(--ink); font-weight: 650; }

/* ------------------------------------------------- what reaches you off the app
   One row per thing that could land in somebody's inbox, saying what will arrive rather than
   naming a category. "Activity" tells nobody what they are agreeing to. */
.switch-row { display: flex; gap: 12px; align-items: flex-start; padding: 12px 0;
  border-bottom: 1px solid var(--line); cursor: pointer; }
.switch-row:last-child { border-bottom: 0; padding-bottom: 0; }
.switch-row input { width: 24px; height: 24px; flex: none; margin-top: 1px;
  accent-color: var(--accent); cursor: pointer; }
.switch-row strong { font-size: 14px; font-weight: 600; }

/* An invite link is there to be copied, so it is a field somebody can select rather than text
   they have to drag across. */
.link-field { width: 100%; font-size: 13px; font-family: ui-monospace, SFMono-Regular, Menlo,
  monospace; padding: 9px 10px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--paper); color: var(--ink); }

/* Adding Chapter to a home screen. On iPhone this is instructions rather than a button, because
   Safari offers nothing a page can call. */
.steps { margin: 0; padding-left: 20px; font-size: 14px; line-height: 1.6; color: var(--muted); }
.steps li { margin-bottom: 4px; }
.steps strong { color: var(--ink); }


/* ------------------------------------------------------------ status messages

   Every confirmation in this app went through toast(), and a toast is a div that appears and
   disappears. A screen reader was told nothing at all: "Link copied", "Date set", "Welcome to
   the club" were all silent. The live region is the fix; this is the styling that goes with it
   and it is unchanged. */
[role="status"] { }

/* Focus. The app leaned on the browser default, which is invisible against several of these
   backgrounds and disappears entirely inside the glass tab bar. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
#tabbar .tab:focus-visible { outline-offset: -3px; }
.sheet :focus-visible { outline-offset: 1px; }

/* A sheet is a modal now: it says so, it can be left with Escape, and the tab key stays inside
   it. This is the visual half. */
.sheet-backdrop { }
.sheet:focus { outline: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* A destructive confirm reads as destructive. The button people press to stop is the plain one,
   because the safe answer should never be the one that takes a second look to find. */
button.primary.danger-solid, .btn.primary.danger-solid {
  background: #9c3535; border-color: #9c3535; color: #fff; }
@media (prefers-color-scheme: dark) {
  button.primary.danger-solid, .btn.primary.danger-solid {
    background: #c25c5c; border-color: #c25c5c; color: #1a1010; }
}


/* Checkboxes were the browser default, 13px square. The WCAG 2.2 AA floor is 24 and a thumb
   wants more than that. This covers the bare ones; the notification switches size themselves. */
input[type="checkbox"] { width: 24px; height: 24px; accent-color: var(--accent);
  flex: none; cursor: pointer; }
/* A checkbox with a label beside it should be hit by tapping the label too, so the target is
   the sentence rather than the box. */
.check-row { display: flex; gap: 10px; align-items: flex-start; min-height: 44px;
  padding: 6px 0; cursor: pointer; }
.check-row input { margin-top: 2px; }

/* The avatar and the handle in a card header both went to the same profile and were two small
   targets side by side. One link, one target, 44px tall. */
.who-link { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1;
  min-height: 44px; text-decoration: none; color: inherit; }
.who-link strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.who-link:hover strong { color: var(--accent); }

/* A page needs an h1 so a screen reader can say what it is. Home and Discover both started at
   h2, because their first heading was a section rather than the page. These two are page titles
   that carry no visual weight beyond what was already there. */
.page-h1 { font-size: 22px; margin: 2px 0 14px; }
/* A sheet's heading is demoted to h2 in sheet(), so two h1s never coexist. It still has to look
   like the title of the thing it opened. */
.sheet-title { font-family: var(--serif); font-size: 22px; line-height: 1.2;
  letter-spacing: -.02em; text-transform: none; color: var(--ink);
  margin: 0 0 6px; font-weight: 600; }

/* An error page's title was a <strong>, which gave a screen reader a page with no heading. It
   is the page title now and looks exactly as it did. */
.empty-title { font-family: var(--serif); font-size: 19px; font-weight: 600;
  text-transform: none; letter-spacing: -.01em; color: var(--ink); margin: 0 0 4px; }

/* Reaching the end of a book. It used to be a dead button that said "Finished chapter 59" and,
   when pressed, set you to chapter 59 again. */
.done-row { margin-top: 12px; padding: 12px 14px; border-radius: 10px;
  background: var(--accent-soft); border: 1px solid var(--accent); }

/* ------------------------------------------------------------ talk on a book

   It used to be a row per chapter, always, in order, whether or not anybody had said anything.
   On a 59 chapter book that is 59 rows mostly reading "no comments", so the discussion that
   does exist is buried inside a list of absences. Two tabs now: what was said, and a grid for
   getting to a chapter. */
.said { margin-bottom: 14px; }
/* CH-96. The note under every box that writes to a club, and the talking points in the wrap-up. */
.talk-note { margin: 8px 0 0; }
svg.ai-sparkle { width: 15px; height: 15px; fill: currentColor; stroke: none; margin-right: 6px; flex: none; }
/* A person's name, wherever it appears, is a link now (CH-116). It has to read as part of the
   sentence rather than as a button: same weight, same colour, and the underline only on the hover
   and focus a pointer or a keyboard brings. The whole tag, avatar included, is one target,
   because the avatar is what a thumb goes for. */
.person-tag {
  display: inline-flex; align-items: center; gap: 8px;
  color: inherit; text-decoration: none; min-width: 0;
  /* A name is a target now, so it has to be one a thumb can hit. 24px is the WCAG 2.2 AA floor
     and the suite fails the build under it; the audit caught two of these the first time these
     links went in, both sized to their own text. */
  min-height: 24px;
}
.person-tag:hover span, .person-tag:focus-visible span { text-decoration: underline; }
.person-tag:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 8px; }
/* An @handle inside somebody's words. Marked by colour rather than by an underline, so a comment
   with three mentions in it does not turn into a row of stripes. */
a.at { color: var(--accent); text-decoration: none; font-weight: 500; }
a.at:hover, a.at:focus-visible { text-decoration: underline; }

.talk-points { margin: 0; padding-left: 22px; }
.talk-points li { margin: 0 0 8px; line-height: 1.5; }
/* A book can hold several sets now, one per stretch of chapters, so each one needs a line of its
   own and a little air between it and the next. */
.dp-set { margin: 0 0 14px; }
/* Three kinds of question under three headings. The heading is a label, not a section title: it
   sits quietly above its list so the questions stay the thing on screen. */
.dp-group + .dp-group { margin-top: 12px; }
.dp-group-head {
  text-transform: uppercase; letter-spacing: .06em; font-weight: 650;
  color: var(--muted); margin: 0 0 6px;
}
#dp-list .dp-group-head { margin-top: 14px; }
#dp-list .dp-group-head:first-child { margin-top: 0; }
.dp-set:last-of-type { margin-bottom: 8px; }
.dp-set > .row { margin-bottom: 6px; }
.tp-row { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 8px; }
.tp-row textarea { min-height: 0; }
/* The shelf's arrows, stacked, so the question keeps its width on a phone. */
.tp-row .tp-moves { flex-direction: column; }
.tp-row .tp-moves .icon-btn { width: 32px; height: 32px; }
/* The sort sits over the list it sorts, small and to the right: a way of looking, not a thing to
   do, so it should not compete with the composer above it. */
.talk-sort { display: flex; align-items: center; justify-content: flex-end; gap: 8px;
  margin: 0 0 10px; }
.talk-sort label { margin: 0; font-size: 12px; }
.talk-sort select { width: auto; max-width: 70%; min-height: 36px; padding: 6px 10px;
  font-size: 14px; }
/* The chapter a comment came from is the way from the list to the chapter itself, so it is a
   target rather than prose, and gets the height one needs. */
.said-from { font-size: 12px; margin: 0 0 2px 2px; }
.said-from a, .said-from span { display: inline-flex; align-items: center; min-height: 24px;
  padding: 2px 0; color: var(--faint); text-decoration: none; }
.said-from a:hover { color: var(--accent); text-decoration: underline; }
.said .comment { background: var(--surface); border: 1px solid var(--line);
  border-radius: 12px; padding: 12px 14px; }

/* 59 rows is a scroll. 59 cells is a glance, and the state of each one was the only thing the
   row was carrying. */
.ch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(46px, 1fr));
  gap: 6px; }
.ch-cell { position: relative; min-height: 44px; display: flex; align-items: center;
  justify-content: center; border-radius: 9px; border: 1px solid var(--line);
  background: var(--surface); color: var(--ink); font: inherit; font-size: 14px;
  font-variant-numeric: tabular-nums; cursor: pointer; }
.ch-cell:hover { border-color: var(--accent); }
.ch-cell.has { border-color: var(--accent); }
.ch-cell.current { background: var(--accent); border-color: var(--accent); color: #fff; }
@media (prefers-color-scheme: dark) { .ch-cell.current { color: #1a1310; } }
/* A chapter you have not finished. Dashed and sunk so it still reads as unfinished at a glance,
   but it is a place you can go now: the page behind it shows the discussion covered with a way to
   uncover it, so hiding the number would hide a door the app has already agreed to open. */
.ch-cell.locked { background: var(--sunk); border-style: dashed; cursor: pointer; }
.ch-cell.locked .ch-n { opacity: .55; }
.ch-dot { position: absolute; top: 5px; right: 6px; width: 6px; height: 6px;
  border-radius: 50%; background: var(--accent); }
.ch-cell.current .ch-dot { background: currentColor; }
.ch-named { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }

/* ------------------------------------------------- saving a quote as a picture */

.x-prev {
  background: var(--sunk, rgba(0,0,0,.06));
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  min-height: 120px;
  display: flex; align-items: center; justify-content: center;
}
.x-prev canvas { display: block; width: 100%; height: auto; max-height: 46vh; object-fit: contain; }

/* --------------------------------------------- a photo of a page, marked

   A photo card is a photograph in a frame rather than a page of type, so it opts out of the
   paper card's padding, minimum height and vertical centring. What it keeps is the shape: same
   corner, same shadow, same small print at the foot. */

.qcard.qphoto {
  display: block; min-height: 0; padding: 12px;
  background: var(--surface); color: var(--ink);
}
.q-shot {
  position: relative; width: 100%; overflow: hidden;
  border-radius: 3px; background: var(--sunk);
  touch-action: none;                 /* the drag is ours, not the browser's scroll */
}
.q-shot img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  user-select: none; -webkit-user-drag: none;
}
.q-shot.pick { cursor: crosshair; }
.q-hl {
  position: absolute; pointer-events: none;
  mix-blend-mode: multiply; opacity: .55; border-radius: 2px;
}
.q-shot.pick .q-hl:not(.live) { pointer-events: auto; cursor: pointer; }
.q-hl.live { opacity: .4; outline: 1px dashed rgba(0,0,0,.35); }
.q-said { margin: 12px 4px 0; font-size: 15px; line-height: 1.5; }
.qphoto .q-attrib { margin: 10px 4px 2px; }

.or-row {
  display: flex; align-items: center; gap: 10px;
  margin: 16px 0 12px; color: var(--faint); font-size: 12px;
}
.or-row::before, .or-row::after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}
.file-row {
  display: inline-flex; align-items: center; gap: 8px;
  width: 100%; justify-content: center; cursor: pointer;
}
.tiny.warn { color: var(--warn); }

/* ------------------------------------- a book's two endings

   The reading closes, and later the club is done with it. Both are states somebody has to be
   able to see at a glance, so both get a line rather than a colour. */
.phase-line {
  margin: 0 0 12px; padding: 10px 12px;
  background: var(--accent-soft); border-radius: 10px; color: var(--ink);
}
.close-row {
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
}
.close-row p { flex: 1 1 100%; }

/* ------------------------------------------- a note, which is not a page

   A paper card says the words were printed in a book. A note is the reader's own, so it sits on
   the card itself in the reading serif, with the book named underneath as a byline. */

.note-post {
  padding: 4px 2px 2px;
}
.note-post p {
  margin: 0; font-family: var(--serif); font-size: 18px; line-height: 1.6;
  white-space: pre-wrap;
}
.note-on {
  margin-top: 12px; font-size: 11px; letter-spacing: .07em; text-transform: uppercase;
  color: var(--faint);
}

/* --------------------------------- what a book is about, and why we should read it

   Two different sentences. The synopsis is reference and folds away; the pitch is somebody in
   the club making a case, so it stays open and is set in the reading serif. */

.synopsis {
  margin: 0 0 16px; padding: 12px 14px;
  background: var(--sunk); border-radius: 10px;
}
.synopsis summary {
  cursor: pointer; font-weight: 600; font-size: 14px;
  min-height: 24px; display: flex; align-items: center;
}
.synopsis summary:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.synopsis p { margin: 10px 0 0; font-size: 14.5px; line-height: 1.55; white-space: pre-wrap; }
.synopsis-card { margin-bottom: 18px; }

.pitch {
  margin: 8px 0 0; font-family: var(--serif); font-size: 15px; line-height: 1.5;
  color: var(--ink);
}
.pitch.big {
  margin: -4px 0 14px; font-size: 17px;
  padding-left: 14px; border-left: 2px solid var(--accent);
}
.pitch .who {
  display: block; margin-top: 6px;
  font-family: var(--sans); font-size: 11px; letter-spacing: .07em;
  text-transform: uppercase; color: var(--faint);
}


/* ------------------------------------------------------------------ quick add

   A row here is a whole sentence, not a word: "Say something about chapter 7" beats "Comment",
   because the point of the sheet is that the reader does not have to work out what applies to
   them right now. The second line carries the state that makes it true. */
.qa-list { display: grid; gap: 8px; }
.qa {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 12px 14px; min-height: 60px;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface); color: inherit;
  font: inherit; text-align: left; cursor: pointer;
}
.qa:hover { background: var(--sunk); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
.qa:active { transform: scale(.995); }
.qa strong { display: block; font-size: 15px; }
.qa .small { display: block; color: var(--muted); margin-top: 2px; }
.qa-i {
  display: grid; place-items: center; flex: 0 0 auto;
  width: 38px; height: 38px; border-radius: 11px;
  background: var(--sunk); color: var(--muted);
}
.qa-i.accent { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
.qa-i svg { width: 20px; height: 20px; fill: none; stroke: currentColor;
  stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.qa-now + .qa-now { margin-top: 14px; }

.inbox-row { align-items: center; gap: 10px; padding: 14px; text-decoration: none;
  color: inherit; margin-bottom: 18px; }
.inbox-row:hover { background: var(--sunk); }

/* The doorway from the feed into Discover. A search bar rather than a tab: see viewFeed. */
.findbar {
  display: flex; align-items: center; gap: 10px;
  width: 100%; min-height: 46px; padding: 0 14px; margin: 0 0 18px;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--sunk); color: var(--muted);
  text-decoration: none; font-size: 14.5px;
}
.findbar:hover { background: var(--surface); border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); }
.findbar svg { width: 18px; height: 18px; flex: 0 0 auto; fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }

/* Confetti. Fixed, unclickable, above the sheet so it reads as landing on top of the moment
   rather than behind it. See src/confetti.js for why reduced motion gets a fade instead. */
.confetti-layer {
  position: fixed; inset: 0; z-index: 80;
  pointer-events: none; overflow: hidden;
}
.confetti-layer.quiet {
  background: radial-gradient(ellipse at 50% 60%,
    color-mix(in srgb, var(--accent) 22%, transparent), transparent 60%);
  animation: quiet-cheer .85s ease-out forwards;
}
@keyframes quiet-cheer { from { opacity: 0; } 35% { opacity: 1; } to { opacity: 0; } }

/* A finish on the feed. The jacket leads, because with no passage to read the book is the
   thing worth looking at. */
.fin { display: flex; gap: 16px; align-items: flex-start; padding: 18px 16px;
  background: var(--sunk); border-radius: 12px; }
.fin-cover { width: 78px; flex: 0 0 auto; border-radius: 4px; display: block;
  box-shadow: 0 1px 2px rgba(28,26,23,.18), 0 8px 18px -6px rgba(28,26,23,.3); }
.fin-cover.blank { height: 112px; display: grid; place-items: center;
  background: var(--surface); border: 1px solid var(--line);
  font-family: var(--serif); font-size: 30px; color: var(--muted); }
.fin-tag { display: block; font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); font-weight: 600; margin-bottom: 3px; }
.fin-title { font-family: var(--serif); font-size: 21px; line-height: 1.2; display: block; }
.fin-stars { display: inline-block; margin-top: 7px; color: var(--accent); letter-spacing: 2px; }
.fin-stars .dim { opacity: .25; }
.fin-say { margin: 9px 0 0; font-size: 14.5px; }

/* ---------------------------------------------------------------- where a comment goes

   One box with a choice above it, replacing the separate "open thread" card. The chips are the
   destination: the club, or a chapter. Sized to the 44px touch target the rest of the app holds to,
   because a chip that is hard to hit is a comment posted to the wrong place. */
.say .chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.chip {
  font: inherit; font-size: 13px; line-height: 1;
  min-height: 34px; padding: 9px 13px;
  border-radius: 999px; border: 1px solid var(--line);
  background: var(--surface); color: var(--muted);
  cursor: pointer;
}
.chip.on {
  background: var(--accent-soft); border-color: var(--accent);
  color: var(--accent); font-weight: 600;
}
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --------------------------------------------------------------- the hub, Sept 15 2026

   Three doors in the header, in line with the logo, because follows, messages and notifications
   are people waiting on you rather than places you go, and they have to be visible from whatever
   page you are on. 40px targets with 4px between them clears the 44px rule once the header's own
   padding is counted, and the three of them plus the wordmark fit inside 320px. */
#chrome-right { gap: 4px; }

.chrome-btn {
  position: relative;
  display: grid; place-items: center;
  width: 40px; height: 40px; padding: 0;
  border: 0; border-radius: 10px;
  background: none; color: var(--muted);
  -webkit-tap-highlight-color: transparent;
}
.chrome-btn:hover, .chrome-btn:focus-visible { background: var(--sunk); color: var(--ink); text-decoration: none; }
.chrome-btn.on { background: var(--accent-soft); color: var(--accent); }
.chrome-btn svg {
  width: 21px; height: 21px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The count sits on the icon rather than beside it. Beside it, three of them would not fit. */
.chrome-btn .n {
  position: absolute; top: 3px; right: 2px;
  min-width: 16px; height: 16px; padding: 0 4px;
  display: grid; place-items: center;
  font-size: 10px; font-weight: 600; font-style: normal; line-height: 1;
  border-radius: 8px;
  background: var(--accent); color: #fff;
  box-shadow: 0 0 0 2px var(--paper);
}

/* --------------------------------------------------------------- follow bar */

.follow-bar { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); }

/* --------------------------------------------------------------- messages */

.dm-row { display: block; color: inherit; }
.dm-row:hover { text-decoration: none; }
.dm-row.unread strong { color: var(--accent); }
/* One line of the last thing said, clipped. A message list where one row is a paragraph is a
   list you cannot scan. */
.dm-last {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dm-when { flex: 0 0 auto; display: grid; justify-items: end; gap: 4px; }
.dm-when .n {
  min-width: 18px; height: 18px; padding: 0 5px;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 600; font-style: normal; line-height: 1;
  border-radius: 9px; background: var(--accent); color: #fff;
}

.dm-log { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 16px; }
.bubble {
  align-self: flex-start; max-width: 80%;
  padding: 9px 13px;
  border: 1px solid var(--line); border-radius: 16px 16px 16px 4px;
  background: var(--surface);
}
/* pre-wrap belongs to the words and to nothing else. On the bubble it also preserved the
   template's own indentation, so every message began with a newline and ten spaces, which pushed
   the text along its first line and stretched every bubble to its 80% cap. */
.bubble .dm-body { display: block; white-space: pre-wrap; overflow-wrap: anywhere; }
.bubble.mine {
  align-self: flex-end;
  border-radius: 16px 16px 4px 16px;
  background: var(--accent-soft); border-color: transparent;
}
.bubble i { display: block; margin-top: 4px; font-style: normal; }

/* The composer is an island, floating at the bottom of the screen above the tab bar.

   It was `position: sticky` before, which does the right thing in a long conversation and the
   wrong thing in a short one: sticky only pins an element once the flow would push it past its
   offset, so in a thread with two messages in it the composer sat wherever the flow put it, which
   is directly under the last bubble, halfway up an empty screen. Ryan, 1 October: "The message
   bar should be at the bottom until selected." Fixed is what he is describing.

   Same glass as the tab bar, and the same translateZ(0) with it. That is not decoration: a fixed
   element with a backdrop-filter is exactly the combination that left the nav bar painted at a
   stale position on a real iPhone while Chromium held it correctly at every scroll position, and
   promoting it to its own layer is what fixed that. Two elements, one bug, one fix. */
.dm-write {
  position: fixed; z-index: 55;
  left: 50%; transform: translateX(-50%) translateZ(0);
  will-change: transform;
  bottom: calc(var(--kb, 0px) + 76px + env(safe-area-inset-bottom, 0px));
  width: min(420px, calc(100% - 24px));

  display: flex; align-items: flex-end; gap: 4px;
  padding: 6px 6px 6px 10px;
  border-radius: 24px;

  background: color-mix(in srgb, var(--surface) 72%, transparent);
  backdrop-filter: blur(22px) saturate(180%);
  -webkit-backdrop-filter: blur(22px) saturate(180%);

  border: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  border-top-color: color-mix(in srgb, #fff 55%, transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb, #fff 45%, transparent),
    0 1px 2px rgba(28, 26, 23, .06),
    0 12px 28px -10px rgba(28, 26, 23, .28);

  transition: bottom .18s ease;
}
@media (prefers-color-scheme: dark) {
  .dm-write {
    background: color-mix(in srgb, var(--surface) 62%, transparent);
    border-top-color: color-mix(in srgb, #fff 16%, transparent);
    box-shadow:
      inset 0 1px 0 color-mix(in srgb, #fff 12%, transparent),
      0 12px 32px -8px rgba(0, 0, 0, .7);
  }
}
@media (prefers-reduced-motion: reduce) { .dm-write { transition: none; } }

.dm-write .field { flex: 1; min-width: 0; margin: 0; }
/* One line until there is something to say. The textarea grows as it fills, up to five lines,
   and the island grows with it. */
.dm-write textarea {
  min-height: 36px; max-height: 120px;
  padding: 7px 10px; border-radius: 18px;
  resize: none; overflow-y: auto;
}
.dm-write .icon-btn { flex: 0 0 auto; width: 34px; height: 34px; }
.dm-write .primary { flex: 0 0 auto; }

/* With the keyboard up the island rides above it and the tab bar gets out of the way, which is
   what every messaging app does and what the screenshot Ryan sent is doing. --kb is set from
   visualViewport, because on iOS a fixed element otherwise sits underneath the keyboard. */
body.kb-up .dm-write { bottom: calc(var(--kb, 0px) + 8px); }
body.kb-up #tabbar { opacity: 0; pointer-events: none; }
#tabbar { transition: opacity .15s ease; }
@media (prefers-reduced-motion: reduce) { #tabbar { transition: none; } }

/* The log has to clear the island, or the newest message is the one hidden behind it. The island
   floats 76px off the bottom to leave the tab bar its room, and is about 52px tall itself, so 92
   was short by the height of a bubble and the last thing said sat underneath it.

   Typing takes the tab bar away, so the room it was holding comes back to the conversation. */
.dm-log { padding-bottom: 150px; }
body.kb-up .dm-log { padding-bottom: 78px; }

/* --------------------------------------------------------------- arranging a shelf, Sept 15 2026

   The shelf itself draws spines and leaning covers in a horizontal run, which is lovely and
   miserable to drag on a phone: 18px targets that overlap on purpose, in a direction that fights
   the page's own scroll. Arranging happens in a list instead, one row per book, full width. */
.arrange { display: flex; flex-direction: column; gap: 6px; }

.arrange-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px; border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface);
}
.arrange-row.lifted {
  box-shadow: var(--shadow);
  border-color: var(--accent);
  transform: scale(1.01);
}
.arrange-row strong { font-size: 14px; }

/* touch-action: none on the handle ALONE. The row still scrolls the sheet under a finger; only
   the grip claims the gesture, which is the difference between a list you can arrange and a list
   you cannot scroll. */
.arrange-grip {
  flex: none; display: grid; place-items: center;
  width: 32px; height: 40px;
  color: var(--faint); cursor: grab;
  touch-action: none; -webkit-tap-highlight-color: transparent;
}
.arrange-grip:active { cursor: grabbing; }
.arrange-grip svg {
  width: 20px; height: 20px;
  fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round;
}
.arrange-moves { flex: none; display: flex; gap: 2px; }
.arrange-moves .icon-btn { width: 36px; height: 36px; }
.arrange-moves .icon-btn[disabled] { opacity: .3; cursor: default; }

/* When a book was read, in the words the reader used: a year on its own when that is all they
   gave, never a month they did not. */
.shelf-when { flex: none; color: var(--faint); white-space: nowrap; }

/* --------------------------------------------------------------- a profile that is closed */
.lock {
  width: 34px; height: 34px;
  fill: none; stroke: var(--faint); stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}

/* --------------------------------------------------------------- placing a profile photo

   The mask is one radial gradient: clear inside the circle, page colour outside it. What shows
   through the clear part is exactly the pixels the canvas will draw, because the circle and the
   canvas are computed from the same three numbers. A preview that disagrees with its output is
   worse than no preview, since there is no way to notice until the wrong crop is on every screen
   you appear on. */
.crop-stage {
  position: relative; overflow: hidden;
  margin: 0 auto;
  border-radius: 14px;
  background: var(--sunk);
  /* The whole square is the drag surface, so the browser must not claim the gesture for scrolling
     the sheet or pinch-zooming the page. */
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  cursor: grab;
  user-select: none;
}
.crop-stage:active { cursor: grabbing; }
.crop-stage:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.crop-stage img {
  position: absolute; top: 0; left: 0;
  transform-origin: 0 0;
  max-width: none;          /* sized in px, and must not be squeezed by the global img rule */
  pointer-events: none;     /* the drag belongs to the stage, not to the picture */
  -webkit-user-drag: none;
}

/* closest-side puts 100% of the gradient at half the square's width, which is the inscribed
   circle exactly. Anything else (the default is farthest-corner) draws a hole the wrong size and
   quietly lies about the crop. */
.crop-mask {
  position: absolute; inset: 0;
  border-radius: 14px;
  pointer-events: none;
  background: radial-gradient(circle closest-side at 50% 50%,
    transparent 0 calc(100% - 1px),
    color-mix(in srgb, var(--paper) 84%, transparent) 100%);
}
/* The rim, on its own element so the gradient above cannot erase it. */
.crop-stage::after {
  content: ""; position: absolute; inset: 0;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--ink) 25%, transparent);
  pointer-events: none;
}

/* A club's own face, beside its name. Sits on the banner's lower edge when there is one, the way
   the pair Christian described does, and stands on its own when there is not. */
.club-face { flex: none; }
.club-banner + .card .club-face,
.club-banner + .row .club-face {
  margin-top: -44px;
  box-shadow: 0 0 0 3px var(--paper);
}

/* The flag on somebody else's message. Quiet until the bubble is under a cursor, because a report
   button that shouts sits on every line of every friendly conversation. */
.bubble { position: relative; }
.bubble-flag {
  position: absolute; top: 2px; right: -30px;
  width: 26px; height: 26px; padding: 0;
  border: 0; background: none; color: var(--faint);
  font-size: 13px; line-height: 1; cursor: pointer;
  opacity: 0; transition: opacity .12s;
}
.bubble:hover .bubble-flag,
.bubble-flag:focus-visible { opacity: 1; }
/* No hover on a touch screen, so there it is simply always there and quiet. */
@media (hover: none) {
  .bubble-flag { opacity: .5; }
}

/* ------------------------------------------------------------------ notices */

/* A notice sits above the tabs on a club page, which is the only place on that page where
   something can be seen whichever tab is open. It has two states and they are deliberately
   very different sizes: unread it is a card with the whole notice in it, and once it has been
   ticked off it is one line. Same information, a twentieth of the space, because a notice left
   up for three weeks at full height is a banner and people learn to scroll past banners. */
.pins { margin-bottom: 20px; }

.pin {
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 10px;
}
.pin.fresh {
  /* The one unread notice earns a left edge in the accent. Three of them do not shout three
     times as loud, they just look like a warning, so the weight is on the edge rather than
     the fill. */
  border-left: 3px solid var(--accent);
}
.pin-head {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--sans); font-size: 12px; letter-spacing: .02em;
  text-transform: uppercase; color: var(--accent);
  margin-bottom: 8px;
}
.pin-head .tiny { text-transform: none; letter-spacing: 0; color: var(--muted); }
.pin-body {
  margin: 0 0 12px;
  white-space: pre-wrap;
  font-family: var(--serif); font-size: 17px; line-height: 1.5;
  color: var(--ink);
  overflow-wrap: anywhere;
}
svg.pin-mark {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}

/* Folded. A button rather than a div, because it is the only thing on it and a person tapping
   anywhere along the line means open it. */
.pin.folded {
  display: flex; align-items: center; gap: 9px;
  width: 100%; text-align: left;
  padding: 9px 13px; margin-bottom: 8px;
  background: var(--sunk);
  border: 1px solid var(--line);
  border-left: 1px solid var(--line);
  color: var(--muted);
  font: inherit; font-size: 14px;
  cursor: pointer;
}
.pin.folded:hover { background: var(--accent-soft); }
.pin-peek {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pin-more {
  flex: none;
  font-family: var(--sans); font-size: 11px; letter-spacing: .04em;
  text-transform: uppercase; color: var(--accent);
}

/* ------------------------------------------------- the club page as a conversation */

/* Ryan, 16 Sept: the club's main page is the conversation about the book being read. Everything
   here exists to get a reader to the comment box and the talk in one screen instead of three.
   The old page spent about 1,900px on identity, tabs and scheduling before the box. */

.club-banner.short { height: 72px; margin-bottom: 0; }

.club-head { align-items: flex-end; gap: 11px; margin: 0 0 3px; }
/* The face sits on the banner's lower edge when there is one. Less overlap than the old header
   because the banner is shorter and a 44px hang would have eaten half of it. */
.club-banner.short + .club-head .club-face { margin-top: -26px; box-shadow: 0 0 0 3px var(--paper); }
.club-name { font-family: var(--serif); font-size: 21px; line-height: 1.2; letter-spacing: -.02em;
  margin: 0; font-weight: 600;
  /* Two lines, then an ellipsis. A club free-typed its own name, so there is no length to design
     around: "Thursday Night Fiction" wraps to two and something longer would push Invite and the
     three dots down the page after it. The cap holds the row's height still whatever it is
     called, and the full name is on the Books page and in the browser tab either way. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere; }
/* flex-end lined the buttons up with the bottom of the name, so a name that grew a second line
   took them with it. They belong beside its first line. */
.club-head > .grow + * , .club-head .tiny-btn, .club-head .icon-btn { align-self: flex-end; }
/* Six badges across two lines said no more than this one grey line does. */
.club-meta { color: var(--faint); font-size: 12.5px; margin: 0 0 16px; }

.club-rule { border: 0; border-top: 1px solid var(--line); margin: 16px 0 14px; }

/* A row you tap, for the things that are a pointer rather than a place: the next date, who is at
   your chapter, a book still being discussed, people waiting to be let in. */
.thin-row {
  display: flex; align-items: center; gap: 11px; width: 100%; text-align: left;
  padding: 10px 12px; margin-bottom: 12px;
  background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
  color: var(--ink); font: inherit; cursor: pointer;
}
a.thin-row:hover, button.thin-row:hover { border-color: var(--accent); text-decoration: none; }
.thin-row .cover { width: 30px; height: 44px; font-size: 13px; box-shadow: none; }
.thin-row .when { flex: none; text-align: center; line-height: 1.05; min-width: 42px; }
.thin-row .when b { font-family: var(--serif); font-size: 17px; display: block; font-weight: 600; }
.thin-row .faces { display: flex; flex: none; }
.thin-row .faces .avatar { margin-right: -8px; box-shadow: 0 0 0 2px var(--surface); }
.thin-row .faces .avatar:last-child { margin-right: 0; }
.thin-row.want { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 22%, transparent); }
svg.chev { width: 16px; height: 16px; flex: none; fill: none; stroke: var(--faint);
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* The book is the subject of the page, so it is a heading rather than a card. A card round it
   costs 32px of padding at exactly the point somebody is trying to reach the talk. */
.book-title { font-family: var(--serif); font-size: 23px; line-height: 1.15; letter-spacing: -.02em;
  margin: 0 0 2px; font-weight: 600; color: var(--ink);
  /* An h2 so the page keeps one h1 (the club) and the levels do not skip. Every h2 rule has to
     be undone here, because the tiny uppercase eyebrow those rules make is the opposite of what
     the subject of the page should look like. */
  text-transform: none; }
.book-head { margin-bottom: 14px; }
.book-head .posline { width: 100%; text-align: left; border: 0; background: none; padding: 0; }

.vote-row { display: flex; align-items: center; gap: 11px; padding: 10px 0;
  border-bottom: 1px solid var(--line); }
.vote-row:last-of-type { border-bottom: 0; }
.vote-row .cover { width: 34px; height: 50px; font-size: 14px; box-shadow: none; }

/* Where everything that is not this week's conversation went. Quiet on purpose: these are
   opened about as often as a club changes book. */
.club-foot { display: flex; flex-wrap: wrap; gap: 8px 16px; padding-bottom: 8px; }
/* 24px is the WCAG 2.2 AA floor for a target and a 13px link is 20px tall on its own, so the
   row is padded rather than the text made bigger. */
.club-foot a, .club-foot .linkish { font-size: 13px; font-weight: 600; color: var(--accent);
  display: inline-flex; align-items: center; min-height: 26px; }
/* Settings sits in the link row rather than the header. Inviting somebody is the control an
   admin reaches for while the club is going well, and Ryan lost it once already when it was
   four sections down; changing the club's name is a monthly job. */
.club-foot .linkish { border: 0; background: none; padding: 0; color: var(--accent);
  font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  /* 26, not 0. A 13px link is 20px tall on its own and 24 is the WCAG 2.2 AA floor for a
     target; the first version of this rule set min-height to 0 and undid the rule above it. */
  display: inline-flex; align-items: center; min-height: 26px; }
.club-foot .linkish:hover { text-decoration: underline; }
/* The admin's controls for the book, quiet and after the fact. They were three tiny buttons in
   the corner of the book card and they crowded the title at phone width. */


/* The club's tabs, restored 16 September. They were panes, I turned them into text links at the
   foot of a long page, and Ryan said what that did: "That has made it even more difficult to
   navigate. I didn't say to remove the tabs." He is right, and a footer is not navigation.

   Links rather than buttons now, because each one is a page, and on every page of the club rather
   than only the first, so the club reads as one place. */
.tabs .tab[href] { text-decoration: none; }
.tabs .tab[href]:hover { color: var(--ink); text-decoration: none; }
.club-head.bare { margin-bottom: 10px; }
.club-head.bare .club-face { margin-top: 0; box-shadow: none; }
/* What is left in the foot is the admin's tools, not a way to get anywhere. */
.club-foot { margin-top: 2px; }

/* ------------------------------------------------------ replies, and photos in the talk */

/* A reply sits under the thing it answers, indented once and no further. parent_id has been on the
   comments table since the first week and nothing ever wrote to it; the database now fills in the
   book and the chapter from the parent, so a thread is covered or open as one piece and an answer
   can never escape the curtain its question is behind. */
.replies {
  margin: 4px 0 0 14px; padding-left: 14px;
  border-left: 2px solid var(--line);
  display: flex; flex-direction: column; gap: 4px;
}
.comment.reply { background: none; padding-left: 0; padding-right: 0; }
.reply-btn { margin-left: auto; }
.reply-slot:empty { display: none; }
.reply-say { margin: 8px 0 0 28px; padding: 12px; }
.reply-say textarea { min-height: 62px; }

/* The picture, before it is posted and after. */
.photo-slot:empty { display: none; }
.photo-pick { position: relative; margin-top: 10px; }
.photo-pick img {
  display: block; width: 100%; max-height: 220px; object-fit: cover;
  border-radius: 10px; border: 1px solid var(--line);
}
.photo-pick .tiny-btn { position: absolute; right: 8px; bottom: 8px;
  background: var(--surface); box-shadow: var(--shadow); }
.said-photo { display: block; margin-top: 10px; }
.said-photo img {
  /* A height before it loads, so a picture arriving does not shove the rest of the conversation
     down the page, and so the link is a real target rather than a 19px sliver while the bytes are
     still on their way. */
  display: block; width: 100%; height: auto; min-height: 160px; max-height: 320px;
  object-fit: cover;
  border-radius: 10px; border: 1px solid var(--line); background: var(--sunk);
}

/* ---------------------------------------------------------------- the club's row of tabs

   Ryan, with a Facebook group open: "I like the little buttons you can scroll horizontally. We
   don't need buttons, we can keep tabs but the side scroll is important." The row already had
   overflow-x, which is not the same as being usable sideways. What was missing was a reason to
   scroll it, something telling you there is more, and a starting position that is not wrong. */

/* The tabs are a tablist and the tools beside them are not. display:contents keeps the semantics
   without putting a box between the scroller and the things it scrolls. */
.tabs-nav { display: contents; }

/* A tool is an action, never a destination, so it never wears the selected underline. Lighter
   than a tab on purpose: the row reads left to right as places first, then things to do. */
.tabs .tab.tool { color: var(--muted); font-weight: 500; }
.tabs .tab.tool:hover { color: var(--ink); border-bottom-color: transparent; }

/* Where the places stop and the tools start. Not a heading, just a breath. */
.tab-split {
  flex: 0 0 auto; align-self: center;
  width: 1px; height: 16px; margin: 0 6px;
  background: var(--line);
}

/* A row with four more things past its right edge looks exactly like a row with nothing there.
   The fade is the only thing that says otherwise, and it is set by paintTabScroll rather than by
   a media query, because whether it overflows depends on the club, the role and the phone. */
/* Out to the edge of the screen.
 *
 * The row used to stop 16px short of it, inside main's padding, so the tab at the end finished in
 * clean air and the whole thing read as a row that had simply run out. Nothing looked cut off,
 * because nothing was. Negative margin takes the scroller out to the page edge and the padding
 * puts the first tab's text back on the gutter, so the row lines up with the club name above it
 * while the tabs at the far end are sliced by the phone itself. That slice is the affordance. */
.tabs[data-bar="club"] { margin-inline: -16px; padding-inline: 16px; }

.tabs[data-more] {
  /* 32px wide against a 31px slice meant the fade covered every pixel of the tab it was there to
     point at: "Notices" read as "No". 20px, and stopping at a third rather than at nothing, so
     half a word stays a word. The fade is the quiet half of this now; the nudge does the work. */
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 20px), rgba(0,0,0,.35));
  mask-image: linear-gradient(to right, #000 calc(100% - 20px), rgba(0,0,0,.35));
}

/* ---------------------------------------------------------------- the three dots

   For what is yours rather than what is the club's. The admin tools are in the row above. */
.tabs + .club-rule { margin-top: 0; }

svg.dots { width: 20px; height: 20px; fill: currentColor; stroke: none; display: block; }

.menu-list { display: flex; flex-direction: column; gap: 2px; margin: 4px 0 18px; }
.menu-row {
  display: block; width: 100%; text-align: left;
  border: 0; background: none; border-radius: 10px;
  padding: 12px 12px; min-height: 44px;
  color: var(--ink); font-family: var(--sans); font-size: 15px; font-weight: 600;
  cursor: pointer;
}
.menu-row .tiny { display: block; margin-top: 2px; font-weight: 400; color: var(--muted); }
.menu-row:hover { background: var(--sunk); }
.menu-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.menu-row.danger { color: #a33; }
.menu-row.danger .tiny { color: var(--muted); }

/* How much room is left in a field, shown only once it is nearly gone. See paintCount.

   flex-basis keeps it on its own line when the field it belongs to sits in a flex row, which is
   most of them: without it the counter becomes a third item in the row and pushes the buttons
   around every time it appears and disappears. */
/* The character counter under a text box. Not a badge: `.badge.count` is the unread number, and
   the two shared this class name for a week, which stretched every unread badge into a full-width
   bar across the card it sat in. The counter rules now skip anything that is a badge. */
.count:not(.badge) {
  display: block; flex: 0 0 100%;
  margin: 4px 2px 0; text-align: right;
  font-family: var(--sans); font-size: 11px; font-weight: 500;
  color: var(--faint);
}
.count:not(.badge)[hidden] { display: none; }
.count.last:not(.badge) { color: var(--accent); }

/* A meet-up that was called off stays on the Dates page, struck through, until its night passes. */
.card.meet-off s { color: var(--faint); }
.meet-admin { border-top: 1px solid var(--line); padding-top: 12px; }

/* Three rings that only ever fill (CH-34). Deliberately not an Apple Watch ring: those empty at
   midnight, and the fear of breaking a streak is the wrong engine for a reading app. */
.marks { padding: 16px 14px; }
.rings { display: flex; gap: 10px; justify-content: space-around; text-align: center; }
.ring-cell { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0; flex: 1; }
.ring { width: 62px; height: 62px; flex: none; }
.ring-track { fill: none; stroke: var(--sunk); stroke-width: 5; }
.ring-fill {
  fill: none; stroke: var(--accent); stroke-width: 5; stroke-linecap: round;
  /* From the top, clockwise, like every other progress ring anybody has ever seen. */
  transform: rotate(-90deg); transform-origin: 50% 50%;
}
.ring-n { font-family: var(--serif); font-size: 15px; font-weight: 600; fill: var(--ink); }
.ring-next { color: var(--faint); }

/* A setup prompt: one quiet line, never a banner. It sits between the club's title row and its
   notices, so it has to read as less important than both. CH-107. */
.nudge {
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  background: color-mix(in srgb, var(--accent-soft) 45%, transparent);
  margin-bottom: 14px;
}

/* The pasted table of contents, as it will land. Every line is listed, not the first handful, so a
   part heading halfway down cannot shift the rest out of sight; past nine or so the list scrolls. */
.toc-preview ol { margin: 0 0 6px; padding-left: 22px; }
.toc-preview li { margin: 2px 0; }
.toc-preview ol.toc-map { max-height: 210px; overflow-y: auto; overscroll-behavior: contain; }


/* --------------------------------------------------- what a message carries, 1 October 2026

   Ryan: photos "in the disappearing format like IG", GIFs, and voice notes. */

/* The tray is the first row of the island, so attaching something grows the island upward
   instead of leaving a second box floating above it. */
.dm-write { flex-wrap: wrap; }
#dm-tray:empty { display: none; }
#dm-tray { flex: 1 0 100%; padding: 2px 4px 6px; }
.dm-tray-row { display: flex; align-items: center; gap: 8px; }
.dm-tray-pic {
  width: 44px; height: 44px; flex: 0 0 auto;
  object-fit: cover; border-radius: 10px; border: 1px solid var(--line);
}

/* A photo that has not been opened is a button, not an image. Loading it IS opening it, and
   opening it is the thing being counted, so nothing is fetched until somebody asks. */
.dm-media { display: block; margin-bottom: 6px; }
.dm-media .tiny-btn { display: inline-flex; align-items: center; gap: 6px; }
.dm-media svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.6; }
.dm-pic, .dm-gif {
  display: block; max-width: 100%; width: 220px; height: auto;
  border-radius: 12px; margin-bottom: 6px;
}
.dm-media audio { width: 220px; max-width: 100%; }
.dm-gone { display: block; margin-bottom: 4px; }

/* The GIF picker. Two columns on a phone, because a GIF you cannot see is a GIF you cannot
   choose, and three across is a postage stamp. */
.gif-grid {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;
  max-height: 52vh; overflow-y: auto; margin-top: 10px;
}
.gif-pick {
  padding: 0; border: 1px solid var(--line); border-radius: 12px;
  background: var(--sunk); overflow: hidden; cursor: pointer;
  min-height: 44px;
}
.gif-pick img { display: block; width: 100%; height: 110px; object-fit: cover; }
.gif-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The numeral on a sealed photo or a voice note: how many looks it is worth, how long it runs.
   Ryan, 1 October: "We don't want words." A digit is not a word, and it is the one thing a
   picture of a camera cannot say by itself. */
.dm-count {
  font-variant-numeric: tabular-nums; font-size: 12px; font-weight: 600;
  letter-spacing: .02em;
}

/* A club book's title is the way to its own page, where the description, the cover, the rating and
   the quotes live. Ryan, 1 October, of a book on the shortlist: "Is there a way to add a synopsis
   to a book that's already in the shortlist?" There was not, because nothing in a club linked to
   the work at all.

   It reads as the title, not as a link, until you touch it. A page whose headings are all blue is
   a page of links rather than a book. */
.book-title a, .vote-row strong a, .club-card .body h3 a { color: inherit; text-decoration: none; }
.book-title a:hover, .vote-row strong a:hover, .club-card .body h3 a:hover,
.book-title a:focus-visible, .vote-row strong a:focus-visible, .club-card .body h3 a:focus-visible {
  text-decoration: underline; text-underline-offset: 3px;
}

/* ------------------------------------------------- 11 October 2026: the review with Christian
   Library, the book menu, the Instagram-shaped profile, Discover's one search box and the club
   Hub. Everything below reuses the tokens above; nothing new in the palette. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.label-h { font-family: var(--sans); font-size: 12px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--faint); margin: 18px 2px 8px; }
button.soft, .btn.soft { background: var(--accent-soft); border-color: var(--accent-soft); color: var(--accent); font-weight: 600; }
button.soft:hover { border-color: var(--accent); }
.grow-btn { width: 100%; margin-top: 10px; min-height: 44px; }
.icon-btn.round { width: 44px; height: 44px; border-radius: 22px; border: 1px solid var(--line); background: var(--surface); flex: none; color: var(--muted); }
.icon-btn.round.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
@media (prefers-color-scheme: dark) { .icon-btn.round.primary { color: #1a1310; } }
label.icon-btn.round { cursor: pointer; }
.dots-btn { width: 44px; height: 44px; flex: none; margin: -8px -8px 0 0; color: var(--muted); }
.strong-link { font-weight: 600; }

.lib-card { padding: 14px; }
.lib-title { font-family: var(--serif); font-size: 20px; line-height: 1.15; margin: 0 0 2px; }
.lib-title a { color: var(--ink); }
.lib-say { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
.lib-say input { flex: 1; min-width: 0; height: 44px; border-radius: 22px; padding: 0 14px; background: var(--paper); }

.menu-list { display: flex; flex-direction: column; margin: 4px -8px 0; }
.menu-item { display: flex; align-items: center; min-height: 46px; padding: 0 12px; border: 0; border-radius: 10px;
  background: none; text-align: left; color: var(--ink); font-size: 16px; font-weight: 400; width: 100%; }
.menu-item:hover, .menu-item:focus-visible { background: var(--sunk); text-decoration: none; }
.menu-item.danger { color: #a33; }
@media (prefers-color-scheme: dark) { .menu-item.danger { color: #e08a7a; } }
.book-menu-head { gap: 12px; align-items: center; padding-bottom: 12px; margin-bottom: 4px; border-bottom: 1px solid var(--line); }

.pf-top { display: flex; align-items: center; justify-content: space-between; margin: 0 0 10px; }
.pf-top .icon-btn { width: 44px; height: 44px; color: var(--ink); }
.pf-handle { font-family: var(--sans); font-size: 18px; font-weight: 600; margin: 0; }
.pf-id { display: flex; align-items: center; gap: 18px; margin-bottom: 12px; }
.avatar.xxl { width: 96px; height: 96px; font-size: 36px; flex: none; box-shadow: 0 0 0 3px var(--surface), 0 0 0 4px var(--line); }
.pf-counts { flex: 1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); text-align: center; gap: 4px; }
.pf-count { display: flex; flex-direction: column; color: var(--ink); }
.pf-count b { font-family: var(--serif); font-size: 22px; font-weight: 500; }
.pf-count span { font-size: 13px; color: var(--muted); }
a.pf-count:hover { text-decoration: none; }
.pf-name { margin-bottom: 12px; }
.pf-full { font-family: var(--serif); font-size: 22px; line-height: 1.15; }
.pf-buttons { gap: 8px; margin-bottom: 14px; }
.pf-buttons > * { flex: 1; justify-content: center; text-align: center; }
.common-card { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; padding: 10px 12px; margin-bottom: 12px; font-weight: 400; }
.common-covers { display: flex; }
.common-covers .cover { width: 26px; height: 39px; font-size: 11px; box-shadow: 0 0 0 2px var(--surface); }
.common-covers .cover + .cover { margin-left: -10px; }
.stats-card { padding: 4px 14px 14px; }
.stats-card .marks { border: 0; padding: 0; margin: 12px 0 0; background: none; box-shadow: none; }
.fav-row { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 4px; }
.fav { display: flex; flex-direction: column; gap: 4px; width: 64px; flex: none; color: var(--ink); }
.fav .tiny { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.search-field input { height: 46px; border-radius: 23px; padding: 0 16px; }
.hub-book .book-title { font-size: 21px; }
/* The counts open their card, so the rule above them divided nothing from nothing (Ryan spotted
   the empty line on the Books page, 11 October). */
.stats-card .numbers { margin-top: 10px; padding-top: 0; border-top: 0; }

/* Reading lists on Discover (#18). One row of chips that scrolls sideways rather than wrapping
   into a wall of eight. */
.list-chips { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 4px; margin-bottom: 12px;
  scrollbar-width: none; }
.list-chips::-webkit-scrollbar { display: none; }
.list-chips .chip { flex: 0 0 auto; }
.list-picks .thin-row:last-child { border-bottom: 0 !important; }

/* The official seal (12 Oct 2026). Gold foil, like a prize sticker, beside a club's name. */
.seal { display: inline-flex; vertical-align: -0.12em; margin-left: 6px; color: var(--seal); }
.seal svg { width: 1em; height: 1em; }
.seal.lg svg { width: 0.85em; height: 0.85em; }
.seal-edge { fill: currentColor; }
.seal-ring { fill: none; stroke: var(--seal-ink); stroke-width: 1.1; opacity: .55; }
.seal-tick { fill: none; stroke: var(--seal-ink); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.official-line { margin: 2px 0 0; font-size: 13px; color: var(--muted); }
:root { --seal: #b8892b; --seal-ink: #fffaf0; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --seal: #d4a64a; --seal-ink: #2a2116; } }
:root[data-theme="dark"] { --seal: #d4a64a; --seal-ink: #2a2116; }

/* First-visit tips (12 Oct 2026). The hole is the spotlight: a clear box over the control whose
   enormous shadow is the dimmed page. No backdrop-filter, since a fixed layer carrying one is the
   shape that made the nav paint stale on iPhone (CH-122). */
.tour { position: fixed; inset: 0; z-index: 2000; }
.tour-hole { position: fixed; border-radius: 12px; box-shadow: 0 0 0 9999px rgba(20, 14, 8, .58);
  transition: top .25s ease, left .25s ease, width .25s ease, height .25s ease; pointer-events: none; }
.tour-tip { position: fixed; background: var(--surface); color: var(--ink, inherit); border-radius: 14px;
  padding: 16px 16px 14px; box-shadow: 0 12px 32px rgba(0, 0, 0, .25); max-width: calc(100vw - 32px);
  transition: top .25s ease, left .25s ease; }
.tour-count { margin: 0 0 4px; color: var(--muted); }
.tour-title { margin: 0; font-family: var(--serif); font-size: 20px; font-weight: 600; line-height: 1.25; }
.tour-body { margin: 6px 0 0; font-size: 15px; line-height: 1.45; }
.tour-tip [hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .tour-hole, .tour-tip { transition: none; } }

/* CH-140: shelf books are buttons now (they open the book menu), so they shed the app's
   button look. Low specificity on purpose: the shelf rules above still decide size and colour. */
button.spine, button.shelf-book, button.shelf-row {
  border: 0; padding: 0; margin: 0; font: inherit; color: inherit; background-color: transparent;
  cursor: pointer; text-align: left; border-radius: 0; box-shadow: none; min-height: 0;
}
button.shelf-row { width: 100%; padding: 10px 0; }
button.spine { background-color: initial; }

/* Christian's notes, 12 Oct 2026. */
.numbers.num-row { display: grid; grid-template-columns: repeat(var(--n, 5), minmax(0, 1fr)); gap: 4px; text-align: center; }
.numbers.num-row span { display: flex; flex-direction: column; align-items: center; line-height: 1.2; white-space: nowrap; }
.numbers.num-row b { font-family: var(--serif); font-size: 20px; font-weight: 600; }
.link-btn { background: none; border: 0; padding: 0; min-height: 24px; font: inherit; color: var(--accent);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer; box-shadow: none; }
.stall-line { margin: 8px 0 0; color: var(--muted); }
.locate-btn { display: inline-flex; align-items: center; gap: 4px; }
.locate-btn .mini { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; }
.place-searching { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
/* A solo book's chapters (12 Oct 2026). */
.chap-list { padding: 2px 0; }
.chap-row { padding: 10px 14px; border-bottom: 1px solid var(--line); }
.chap-row:last-child { border-bottom: 0; }
.chap-row.shut strong { color: var(--muted); font-weight: 500; }
.curtain { background: var(--sunk); border: 1px dashed var(--line); }
a.curtain { color: var(--muted); text-decoration: none; }
.wn-card .curtain { padding: 12px 14px; }

/* Interests, goal and shelf decor (CH-33, CH-137), 13 October 2026. */
.pf-tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 10px; }
.pf-tags .tag-chips { margin: 0; }
.tag-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.chip.tag { text-decoration: none; }
.chip.tag.waiting { opacity: .6; border-style: dashed; }
.tag-pick { max-height: 38vh; overflow-y: auto; }
.badge.goal-met { display: inline-flex; align-items: center; gap: 4px; background: #f6ecc9; color: #6e5410; border-color: #e3cf86; }
.laurel-i { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
@media (prefers-color-scheme: dark) { .badge.goal-met { background: #3a3116; color: #f0d27a; border-color: #5b4b1c; } }
.goal-row { width: 100%; text-align: left; }
.goal-bar { display: block; height: 4px; border-radius: 2px; background: var(--line); margin-top: 6px; overflow: hidden; }
.goal-bar span { display: block; height: 100%; background: var(--accent); }
.goal-row.met .goal-bar span { background: #c9a227; }
.shelf-unit[data-wall="sage"]    { --wall-1:#dfe7d6; --wall-2:#cfdac3; }
.shelf-unit[data-wall="dusk"]    { --wall-1:#d9dbe8; --wall-2:#c5c8db; }
.shelf-unit[data-wall="green"]   { --wall-1:#3f5a48; --wall-2:#2f4637; }
.shelf-unit[data-wall="oxblood"] { --wall-1:#6b2b2b; --wall-2:#521f20; }
.shelf-unit[data-wall="gold"]    { --wall-1:#f3e3b0; --wall-2:#e4cd84; }
.swatch.wall-sage { background: #dfe7d6; } .swatch.wall-dusk { background: #d9dbe8; }
.swatch.wall-green { background: #3f5a48; color: #fff; } .swatch.wall-oxblood { background: #6b2b2b; color: #fff; }
.swatch.wall-gold { background: #f3e3b0; }
.swatch.wall-sage, .swatch.wall-dusk, .swatch.wall-gold { color: #1c1a17; }
.trinket { height: var(--row); flex: 0 0 auto; display: flex; align-items: flex-end; padding: 0 6px var(--plank); }
.trinket svg { width: 34px; height: 48px; display: block; }
