/* LittleBookMatch — one stylesheet, shared by the static question page and the server-rendered
   pages, so the two can never drift apart.

   ⚠️ The reader is the PARENT, not the child. Every children's-book site reaches for primary
   colours and rounded cartoon type, and the effect is a site that looks like it is for a four-year-
   old rather than for the adult choosing on their behalf. This is warm and bookish and grown-up:
   paper, ink, and one accent.

   ⚠️ THAT RULE SURVIVED THE 2026-08-21 REPALETTE — it was the thing that decided its direction.
   The first outside user called the site "too grown up... too much of a list", which reads as an
   argument against the paragraph above. But she cited MORI as the reference, and Mori is
   emphatically grown-up — "stylish parenting, not childish". So the fault was never maturity:
   THE SITE WAS NOT TOO GROWN-UP, IT WAS TOO BARE. Fix richness, not register. */

:root {
  /* ⚠️ A WARM CREAM GROUND, AND THIS REVERSES AN EARLIER INSTRUCTION — read this before "fixing" it.
     On 2026-08-18 the owner had cream taken OFF this site: it was built on the same #FBF7F0 and
     terracotta as every other site in the folder, and their objection was that a beige reading-room
     palette borrowed from a statistics consultancy is the wrong register for toddlers' books. The
     ground went white.

     On 2026-08-21 the owner chose the MORI register, which is cream again — and confirmed it after
     the contradiction was put to them explicitly. The two calls are not the same call: cream
     arrived by COPYING the folder last time and arrives ON PURPOSE this time, from a premium baby
     brand picked for this exact audience. Their own words on the difference: "Mori is not cream. or
     at least not the same cream i reject back then. maybe because of the accent i guess."

     ⛔ So the accent is SAGE, deliberately not the terracotta that was rejected.

     ⚠️ AND ON 2026-09-07 THE GROUND WENT BACK TO WHITE, WHICH IS NOT A THIRD REVERSAL — read this
     before "correcting" it either way. Looking at the site months later the owner said the cream
     was a surprise: what they had chosen on 2026-08-21 was the Mori REGISTER — the wordmark, the
     serif, the sage buttons, the ladder — and the ground came along with it in the same commit
     without ever being put to them on its own. Their words: "i was looking at the text font,
     buttons, ladder etc. (all correct choices indeed) but not on the background colour... i still
     wanted the white bright background."

     ⛔ SO EVERY MORI DECISION SURVIVES AND ONLY THE PAPER MOVED. Sage accent, serif h1 and book
     titles, sans for the questions: all unchanged. The 2026-08-18 objection is still answered,
     because it was never about white — it was about a beige reading-room palette borrowed from a
     statistics consultancy, and none of that is here.

     ⚠️ THE FIVE STEP COLOURS STAY EARTHY. They were switched to the bright 2026-08-18 set for about
     an hour on 2026-09-07 and the owner came back to these after looking at both whole homepages.
     ⭐ So the ground and the ladder were decided SEPARATELY, one at a time, which is the whole point
     — bundling them is what caused the confusion in the first place. Every version this site has
     had, and the two-way homepage comparison:
     https://claude.ai/code/artifact/7b0e1ed9-ce05-43b5-8f36-bf3cac0b17f2
     https://claude.ai/code/artifact/14175dc1-5895-4dcd-a261-c9a537c16936 */

  /* ⛔ CARDS ARE WHITE ON WHITE, SO THE SHADOW IS LOAD-BEARING AND NOT DECORATION. On cream a
     hairline border was enough and the shadow was belt-and-braces; on white the border does
     nothing on its own and the results read as a flat list. This was learned the hard way on
     2026-08-18 (playbook principle 31) and the shadow written then is what makes the 2026-09-07
     return to white cost nothing. Do not "simplify" it away. */
  --page:      #FFFFFF;
  --card:      #FFFFFF;
  --sugar:     #F3F1ED;   /* sand: chips, empty covers, input wells, quiet bands */
  --ink:       #2A2521;
  --body:      #574E45;
  --mute:      #8B8074;
  --line:      #E7E2DA;

  /* ⚠️ ONE BRAND ACCENT, AND IT CARRIES NO MEANING. Playbook principle 13 — the rung colours below
     say something, so the brand must not be any of them.
     ⚠️ It briefly WAS one, and the fix is recorded on --step-3 rather than here: sage shipped on
     2026-08-21 against a rung 3 of #6E8C63, thirteen points apart in one channel. The owner's call
     on 2026-08-22, after seeing it live, was to keep the sage button and move the rung instead. */
  --accent:    #6F7F63;
  --accent-in: #FDFBF7;
  --accent-sf: #F0F3EE;

  /* ⚠️ TEXT AND RULES ON THE DARK FOOTER. Not new hues — --ink, warmed paper, and the accent
     lightened just enough to clear the dark ground. See the footer rules for why there is a dark
     area on this site at all. */
  --on-ink:      #EDE8E0;
  --on-ink-mute: #A9A198;
  --on-ink-line: #3D3630;
  --accent-lt:   #9DB08D;

  /* ⚠️ THE LADDER IS THE PRODUCT, SO THE LADDER CARRIES THE COLOUR. Five steps, five hues, warm to
     cool as the child grows. This is the one place colour MEANS something here.
     ⛔ Never show these as an age. They are steps, and the whole positioning rests on that.

     ⛔ THESE ARE THE VISITOR'S FIVE STEPS, NOT THE ENGINE'S FOUR RUNGS. Renamed from --rung-N on
     2026-08-22 because they had stopped meaning what their name said, which is how a palette drifts
     back. Question 1 offers five choices; the engine sorts on four levels, because pressing a
     squeaker and working a flap are one level with two shapes of book. The mapping lives in ONE
     place — STEPS and stepFor() in src/lib/classify.js — and both renderers read it from there.
     ⚠️ Three of these moved meaning that day: forest was the picture book and is now the flaps,
     slate was the longer story and is now the picture book, and plum was "first chapters", which
     the site does not offer at all, and is now the longer story. */
  --step-1:    #C0684C;   /* clay   — chewing it, patting it, pointing at things */
  --step-2:    #C08F42;   /* ochre  — touching, pressing, feeling */
  /* ⛔ FOREST, NOT SAGE, AND THE DISTANCE FROM --accent IS THE POINT. This was #6E8C63 against a
     sage accent of #6F7F63 — thirteen points apart in a single channel, i.e. the same green — so
     the button read as though it meant something on the ladder, on a site whose one rule is that
     colour only appears where it means something. Owner's decision 2026-08-22, having looked at it
     live: keep the sage button, move the step. ⚠️ Two greens still share the page by design; if
     this ever drifts back towards the accent, the fault returns silently. */
  --step-3:    #4F7A4A;   /* forest — lifting flaps and pop-ups */
  --step-4:    #5C7D96;   /* slate  — sitting through a picture book */
  --step-5:    #87688F;   /* plum   — a longer story */
  --step:      var(--step-4);   /* fallback for anything with no step number on it */

  --radius:    16px;
  --measure:   34rem;
  --cover-w:   92px;

  /* ⚠️ THE TYPEFACES ARE TOKENS NOW, and the reason is measured — see the @font-face block below. */
  --font-display: 'Fraunces', Georgia, serif;
  --font-body:    'Karla', system-ui, sans-serif;

  /* ⚠️ A REAL SIZE LADDER, AND IT IS MEASURED AGAINST COMPARATORS RATHER THAN CHOSEN BY EYE.
     PLAYBOOK, from the weddingvenuelist "it feels like Wikipedia, not an established site"
     post-mortem: "measure the type scale before anything else… a document has a flat hierarchy, a
     designed page has a big jump. Strongest single cue, cheapest single fix." Established
     comparators run their h1 at 2.5–3× body.

     ⛔ WHAT THAT MEASUREMENT FOUND HERE, 2026-09-17, against the 17px body:

         h1 on a phone   33.6px   1.98×   below the range
         h1 on desktop   46.4px   2.73×   fine
         h2              21.6px   1.27×   ⛔
         h2 in the quiz  18.6px   1.09×   ⛔
         h2 on guides    17.9px   1.05×   ⛔

     The h1 was fixed at some point and NOTHING BELOW IT EVER WAS. A section heading five per cent
     bigger than the paragraph under it is Wikipedia's hierarchy exactly, and the phone h1 — which
     is how a parent actually sees this — was under the range too.

     ⚠️ THE LADDER IS SIX RUNGS AND EVERY STEP IS A VISIBLE JUMP: page title, section heading, a
     question the parent answers, a sub-head inside prose, a book title, body. If two of them ever
     end up within about 10% of each other, one of them is not doing a job.
     test/type.test.js holds the ratios, because PLAYBOOK: "a prose target nobody can check drifts
     back within months." */
  --t-h1:    clamp(2.45rem, 9.5vw, 3.2rem);   /* 39.2px phone → 51.2px desktop  (2.31× → 3.01×) */
  --t-h2:    clamp(1.72rem, 6vw, 2rem);       /* 27.5px → 32px                  (1.62× → 1.88×) */
  --t-q:     1.45rem;                         /* 23.2px — a question, not a shout   (1.36×)     */
  --t-h3:    1.25rem;                         /* 20px — a sub-head inside prose     (1.18×)     */
  --t-title: 1.18rem;                         /* 18.9px — a book title on a card    (1.11×)     */
  --t-lede:  1.12rem;                         /* 17.9px                                         */
  /* ⚠️ The smallest rung: a bold label heading inside a feature list, at about body size.
     It IS part of the ladder even though it does not grow — .why-list h3 and
     .where-list h3 sat at 1rem and 1.02rem, two sizes doing one job. PLAYBOOK:
     "inconsistency reads as amateur on its own, separately from whichever style is better." */
  --t-label: 1.02rem;
  --t-small: .88rem;
  --t-micro: .8rem;

  /* ⛔ Light-only, declared. See the block below — this is the line that stops the browser
     darkening the form controls and scrollbars on a dark OS. */
  color-scheme: light;
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   TYPEFACES — FRAUNCES + KARLA, SELF-HOSTED. Chosen by the owner 2026-09-17 off a ?v= switch on
   the real homepage, against Literata+Figtree, Newsreader+PublicSans, and the stack the site
   shipped with. The switch and the losing pairs are deleted; the reasoning is here.
   ═══════════════════════════════════════════════════════════════════════════════════════════════

   ⛔ WHY SELF-HOSTING, because this was the single biggest cause of "the site looks like it is from
   the 2000s" and it was invisible to everyone who read the code.

   Until today the stack asked for `'Avenir Next', Avenir, 'Segoe UI', system-ui` and
   `'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia`. Checked against the
   fonts actually installed on the owner's Ubuntu desktop, 2026-09-17:

       Avenir Next ✗   Avenir ✗   Segoe UI ✗   Iowan Old Style ✗
       Palatino Linotype ✗   Palatino ✗   Book Antiqua ✗   Georgia ✓

   `system-ui` and `sans-serif` both resolve to **Noto Sans** there. So the owner had been judging
   this site in **Noto Sans + Georgia** — Google's deliberately characterless fallback face and a
   1996 Microsoft screen serif. An iPhone visitor got Avenir Next + Iowan Old Style, a Windows
   visitor Segoe UI + Palatino Linotype, an Android visitor Roboto + Noto Serif. **Four different
   sites, and the worst of the four was the one being judged.**

   ⚠️ A FONT STACK IS A LIST OF WISHES. The only way to know which one is granted is to ask the
   machine — `fc-list` and `fc-match`, not a reading of the CSS. The same mistake was made once
   before on this project (see the `body` rule below: a rounded stack that only existed on Apple
   devices) and was "fixed" to another Apple-first stack, because nobody asked the machine either
   time. PLAYBOOK mechanic 1 and principle 36 both say self-host; three of the five sibling sites
   already did — northwoodmetrics Inter + Source Serif 4, thelocalcard DM Sans + Playfair Display,
   webdesignsimply Inter. ⛔ THAT IS WHY NEITHER OF THOSE FOUR FAMILIES IS USED HERE: PLAYBOOK
   requires the type pairing to differ per project, after three sites in this folder became hard to
   tell apart by following one recipe identically.

   ⚠️ TWO RANGES PER FACE, AND THE SPLIT IS NOT DECORATION. `latin` carries the Latin-1 accents the
   corpus needs (á é í ó ö ú ü ¿); `latin-ext` carries ō, which it does not — verified glyph by
   glyph with fontTools rather than assumed. The browser fetches latin-ext ONLY on a page that
   actually contains one of its characters, which is about six records out of 2,991, so it costs
   nothing on every other page.

   ⚠️ AND THE THIRD CASE IS NOT A FONT PROBLEM AT ALL: some corpus records spell an accent as a
   letter plus a COMBINING mark (Lesléa Newman is `e` + U+0301), and U+0301 is in neither range.
   Normalising the text to NFC turns that pair into a single `é`, which both ranges do carry. That
   is done at the render boundary — see `esc()` in src/pages.js. ⛔ Do not "fix" it by widening the
   subset; the decomposed form would still break the next face anyone chooses.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */

@font-face { font-family: 'Fraunces'; src: url('/fonts/fraunces.woff2') format('woff2-variations');
             font-weight: 100 900; font-style: normal; font-display: swap;
             unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308,
                 U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                 U+FFFD; }
@font-face { font-family: 'Fraunces'; src: url('/fonts/fraunces-ext.woff2') format('woff2-variations');
             font-weight: 100 900; font-style: normal; font-display: swap;
             unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
                 U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                 U+2113, U+2C60-2C7F, U+A720-A7FF; }

@font-face { font-family: 'Karla'; src: url('/fonts/karla.woff2') format('woff2-variations');
             font-weight: 200 800; font-style: normal; font-display: swap;
             unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308,
                 U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                 U+FFFD; }
@font-face { font-family: 'Karla'; src: url('/fonts/karla-ext.woff2') format('woff2-variations');
             font-weight: 200 800; font-style: normal; font-display: swap;
             unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
                 U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                 U+2113, U+2C60-2C7F, U+A720-A7FF; }

/* ⚠️ REAL ITALICS, NOT SLANTED UPRIGHTS. PLAYBOOK principle 30b — a faked italic is invisible
   unless you look for it, and the body copy uses <em> in the hero lede, the why-list and the
   footer. Karla's italic is a separate file because it is a separate design. */
@font-face { font-family: 'Karla'; src: url('/fonts/karla-italic.woff2') format('woff2-variations');
             font-weight: 200 800; font-style: italic; font-display: swap;
             unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308,
                 U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                 U+FFFD; }
@font-face { font-family: 'Karla'; src: url('/fonts/karla-ext-italic.woff2') format('woff2-variations');
             font-weight: 200 800; font-style: italic; font-display: swap;
             unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
                 U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
                 U+2113, U+2C60-2C7F, U+A720-A7FF; }

/* ⛔ THERE IS NO DARK THEME, AND THAT IS A DECISION — do not "add it back" as an improvement.
   Owner's instruction, 2026-08-22. A full warm dark palette existed here and was deleted: it
   doubles the surface every future colour change has to be checked against, and this site is not
   live, has no users asking for it, and has a palette chosen from a specific reference.
   See PLAYBOOK "Dark mode is a cost, not a feature".

   ⚠️ `color-scheme: light` on :root is the load-bearing half and is NOT optional. Deleting the
   media query alone leaves the browser free to darken things it renders itself — the country
   `<select>`, the text inputs, the scrollbars — so a visitor on a dark OS gets a cream page with
   black form controls. Light-only is something you declare, not something you get by default. */

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--page);
  color: var(--body);
  /* ⚠️ THE OLD STACK ASKED FOR A ROUNDED FACE IT NEVER LOADED. It was `ui-rounded, 'SF Pro
     Rounded', 'Nunito', …` with no @font-face and no font file anywhere in this repo — so the
     roundness the comment described existed ONLY on Apple devices, and every Android, Windows and
     Linux visitor silently got Segoe UI or system-ui. Nobody could see that from the source.
     A humanist sans that degrades honestly, and no promise of a face we do not ship. */
  font: 17px/1.6 var(--font-body);
  -webkit-text-size-adjust: 100%;
}

.wrap { max-width: var(--measure); margin: 0 auto; padding: 0 1.15rem 4rem; }

/* ⚠️ SERIF FOR THE DISPLAY HEADLINE ONLY, and this is a narrower change than it looks. The rule
   set in 2026-08-18 — "a book title should look like a book title; a question a parent taps should
   not" — is intact: h1 and book titles are serif, and the QUESTION headings below stay sans. */
h1, h2, h3 { color: var(--ink); line-height: 1.15; margin: 0 0 .5rem; font-weight: 800; }
h1 {
  font-family: var(--font-display);
  font-weight: 600;   /* a serif at 800 is a slab; the size is doing the work */
  font-size: var(--t-h1); letter-spacing: -.015em; text-wrap: balance;
}
h2 { font-size: var(--t-h2); letter-spacing: -.015em; text-wrap: balance; }

/* ⚠️ THE SECOND HALF OF A SECTION HEADING, and it is the other thing every established comparator
   does that this site did not. counciltaxchecker, swimmingpoolsnearme, wedissimo and hardcover all
   run a small coloured uppercase label above the heading — BROWSE BY TYPE, SERVICES, SUPPLIERS —
   and it is what makes a heading read as the top of a designed section rather than as the next
   paragraph in a document.

   ⚠️ ONE PER SECTION, NOT ONE PER HEADING. The browse section has three h2s and takes a single
   eyebrow; three in a row would read as a list of labels.

   ⛔ IT MUST NOT RESTATE THE HEADING UNDER IT. PLAYBOOK's cheap repetition check is "every eyebrow
   against the H1 below it" — "SOURCES / Where the books come from" says one thing twice and wastes
   the only line on the page that could have said something else. */
.eyebrow {
  display: block; margin: 0 0 .45rem;
  font: 700 var(--t-micro)/1.3 var(--font-body);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent);
}
p { margin: 0 0 1rem; }
a { color: var(--accent); }

/* ---- masthead ---------------------------------------------------------------------------- */

/* ⚠️ The h1 and lede moved OUT of here and into .hero on 2026-08-21, so this is the wordmark and
   the ladder alone and wants tighter padding than when it carried the headline too. The results
   page (src/pages.js) still keeps its h1 inside header — the two pages differ here on purpose. */
/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   ⚠️ THE MASTHEAD, AND THE REASON IT GAINED A NAV ON 2026-09-17.
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   PLAYBOOK, from the weddingvenuelist "it feels like Wikipedia" post-mortem: "ask what on the page
   is a control. No nav, no persistent search, no result count, no sort, no saved items = a
   document, however good the data is."

   ⛔ THIS SITE HAD NO NAVIGATION ON ANY PAGE. The masthead was a wordmark and five 18×3.5px bars.

   ⛔ AND THE WORSE HALF: THE QUIZ — THE ENTIRE PRODUCT — EXISTED ON THE HOMEPAGE ONLY. 38 of the
   site's 39 indexable pages are /books-about/… and /guides/…, so a parent arriving from a search
   engine landed somewhere with no way to use the thing the site is for, short of finding a text
   link at the very bottom. PLAYBOOK names this trap in as many words: "search on the homepage only
   is worse than it sounds — a site living on search traffic is entered at its DEEP pages, so almost
   nobody who arrives can search at all."

   ⚠️ Every reference site the owner named has a nav bar, and counciltaxchecker goes further and puts
   the working postcode field in the sticky header of every page.

   ⛔ SO EVERY SERVER-RENDERED PAGE CARRIES A "Find a book" BUTTON AND THE HOMEPAGE DOES NOT — it
   would point at the form six inches below it. That asymmetry is deliberate; a test holds it.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */
header {
  padding: 1.4rem 0 1.2rem;
  display: flex; align-items: center; justify-content: space-between;
  gap: .8rem 1.2rem; flex-wrap: wrap;
}
.brand { min-width: 0; }

.nav {
  display: flex; align-items: center; gap: .15rem; flex-wrap: wrap;
  font: 500 .93rem/1 var(--font-body);
}
.nav a {
  color: var(--body); text-decoration: none;
  padding: .5rem .6rem; border-radius: 999px;
  transition: color .12s, background .12s;
}
@media (hover: hover) { .nav a:hover { color: var(--ink); background: var(--sugar); } }
.nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The one filled control in the masthead. ⚠️ Same sage as the main button, deliberately — a parent
   who has seen "Show me five books" should recognise this as the same door. */
.nav-cta {
  background: var(--accent); color: var(--accent-in) !important;
  font-weight: 700; margin-left: .3rem;
  padding: .55rem .9rem;
}
@media (hover: hover) { .nav-cta:hover { background: #5F6E55; color: var(--accent-in) !important; } }

/* ⚠️ At 375px the wordmark and four nav items do not fit on one line, so the nav wraps to its own
   full-width row rather than squeezing. Checked at 375px — PLAYBOOK principle 3e. */
@media (max-width: 27rem) {
  header { gap: .6rem; }
  .nav { width: 100%; margin-left: -.6rem; }
  .nav-cta { margin-left: auto; }
}

/* ⚠️ CHOSEN FROM FOUR STUDIES, 2026-08-21 — the owner's words were "the word mark two, but on one
   line and with serif like in one": option two's ladder rule, option one's single-line serif.
   The old one was plain text at 1rem/700, which is SMALLER than the 17px body copy beneath it and
   about a third of the h1 — measured, and it is why it read as a label rather than as a name.

   ⛔ No background tile. Four tile designs were tried on weddingvenuelist and the winner was no
   tile at all — PLAYBOOK principles 33 and 36. */
.wordmark {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 1.32rem; font-weight: 600; letter-spacing: .005em;
  color: var(--ink); text-decoration: none;
}
/* ⚠️ The tint on "Book" was dropped deliberately: the ladder rule sits directly beneath the name,
   so tinting the word too would put colour in two places when the whole rule is that colour lives
   on the ladder. The span stays as the hook — set a colour here to bring it back. */
.wordmark span { color: inherit; }

/* ⚠️ The ladder in the masthead, one bar per rung, in the rung colours. It is the only decoration
   on the page and it is not decoration: it is the product's own scale, so a parent meets the idea
   before the first question rather than after the answer. Hidden from screen readers — it repeats
   what the questions say.
   ⚠️ Bars rather than dots since 2026-08-21. As five loose circles it read as five unexplained
   coloured dots; as a rule under the name it reads as part of the wordmark, which is what the
   chosen study does with it. Same five elements, same markup, same colours. */
.ladder-dots { display: flex; gap: .22rem; margin: .5rem 0 0; }
.ladder-dots i { width: 1.15rem; height: .22rem; border-radius: 2px; display: block; }
.ladder-dots i:nth-child(1) { background: var(--step-1); }
.ladder-dots i:nth-child(2) { background: var(--step-2); }
.ladder-dots i:nth-child(3) { background: var(--step-3); }
.ladder-dots i:nth-child(4) { background: var(--step-4); }
.ladder-dots i:nth-child(5) { background: var(--step-5); }

.lede { font-size: var(--t-lede); color: var(--body); margin-top: .9rem; max-width: 34rem; }

/* ---- the front ---------------------------------------------------------------------------- */

/* ⚠️ Full-bleed the honest way: this section sits OUTSIDE .wrap and carries its own .wrap inside.
   The other way — negative margins cancelling .wrap's padding — makes the two numbers a pair, and
   changing either one alone scrolls the whole page sideways (weddingvenuelist, 2026-08-20). */
.hero { background: var(--sugar); padding: 2.2rem 0 2.6rem; margin-bottom: .5rem; }
.hero h1 { margin-bottom: .2rem; }
.hero-copy { min-width: 0; }

.hero-note {
  display: flex; align-items: center; gap: .45rem;
  margin: .8rem 0 0; font-size: .88rem; color: var(--mute);
}

/* The primary action is an <a> here and a <button> on the form, so .go has to survive being both. */
.go--link {
  display: block; text-align: center; text-decoration: none;
  margin-top: 1.3rem; padding: .95rem 1.2rem;
}

/* Decorative, aria-hidden. Overlapping and slightly staggered so it reads as a shelf rather than a
   row of product shots — the covers ARE the visual interest on a page with no photography. */
.hero-covers {
  list-style: none; margin: 1.8rem 0 0; padding: 0;
  display: flex; gap: .5rem; min-width: 0;
}
.hero-covers li { flex: 1 1 0; min-width: 0; max-width: 6.5rem; }
.hero-covers li:nth-child(even) { margin-top: 1.1rem; }
.hero-covers img {
  width: 100%; aspect-ratio: 3 / 4; object-fit: cover; display: block;
  border-radius: 5px; background: var(--card);
  box-shadow: 0 1px 2px rgba(42, 37, 33, .05), 0 10px 22px -16px rgba(42, 37, 33, .5);
}

/* ---- the two explaining sections ---------------------------------------------------------- */

.band { background: var(--sugar); padding: 2.4rem 0 2.6rem; margin-top: 3rem; }
.band-lede { color: var(--body); max-width: 34rem; margin-bottom: 1.4rem; }

.steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.steps li {
  display: grid; grid-template-columns: 1.15rem 1fr; align-items: baseline; column-gap: .8rem;
  font-size: 1rem; color: var(--ink);
}
/* ⚠️ The hint sits UNDER the step, in the second column, so it lines up with the words rather than
   with the colour bar. Same shape as the option cards in question 1, which is the point. */
.steps li small {
  grid-column: 2; display: block;
  font-size: .88rem; color: var(--mute); margin-top: .1rem;
}
.steps-bar {
  width: 1.15rem; height: .22rem; border-radius: 2px; background: var(--step);
  align-self: center; position: relative; top: -.05rem;
}
.steps li[data-step='1'] { --step: var(--step-1); }
.steps li[data-step='2'] { --step: var(--step-2); }
.steps li[data-step='3'] { --step: var(--step-3); }
.steps li[data-step='4'] { --step: var(--step-4); }
.steps li[data-step='5'] { --step: var(--step-5); }

.why { margin-top: 2.8rem; }
.why-list { list-style: none; margin: 1.3rem 0 0; padding: 0; display: grid; gap: 1.4rem; }
.why-list h3 { font-size: var(--t-label); margin-bottom: .25rem; font-weight: 700; }
.why-list p { margin: 0; font-size: .96rem; color: var(--body); }

/* ---- widths ------------------------------------------------------------------------------- */

/* ⚠️ THE SITE HAD NO WIDTH BREAKPOINTS AT ALL until 2026-08-21 — `@media` appeared four times and
   not one was dimensional, so .wrap was capped at 34rem on every screen and a laptop got a 544px
   ribbon of stacked boxes in a field of cream. Measured, and it is most of why the first outside
   user said it looked like a list rather than a website.
   ⚠️ Widening the page does NOT mean widening the prose — running text stays near 34rem above, or
   the lines get too long to read. What widens is the LAYOUT. */
@media (min-width: 46rem) {
  :root { --measure: 46rem; --cover-w: 108px; }
  .hero { padding: 2.6rem 0 2.4rem; }
  /* The covers get the larger share: they are the only picture on the page, and at an equal split
     five of them come out ~70px wide, which reads as thumbnails rather than as a shelf. */
  .hero-in { display: grid; grid-template-columns: 1fr 1.12fr; gap: 2.4rem; align-items: center; }
  .hero-covers { margin-top: 0; }
  .hero-covers li { max-width: 8.5rem; }
  .go--link { display: inline-block; width: auto; padding: .95rem 1.7rem; }
  /* Two columns of options: five cards down one narrow column is the "list" complaint itself. */
  .options { grid-template-columns: 1fr 1fr; }
  .why-list { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.6rem; }
  .go { max-width: 22rem; }
}

@media (min-width: 64rem) {
  :root { --measure: 58rem; }
  /* ⛔ The steps list stays ONE COLUMN at every width. Owner's instruction, 2026-08-22, after
     seeing it split in two on a desktop. It is a ladder — a sequence read top to bottom — and two
     columns turn it into a grid you read across, which is the one thing it must not look like. */
}

/* ---- the questions ----------------------------------------------------------------------- */

.q { margin: 1.9rem 0; }

.q > h2 {
  font-size: var(--t-q);
  /* ⚠️ The question headings are SANS, deliberately, and the comment that used to sit here said
     "so the question stays serif" — which had been untrue since the 2026-08-18 repalette and was
     still here on 2026-08-21. A question a parent taps is an instruction, not a passage of prose;
     the serif is for the h1 and the book titles. Inherits the body face. */
}
.q .hint {
  font: 400 .88rem/1.5 inherit;
  color: var(--mute); margin: -.15rem 0 .85rem;
}

.options { display: grid; gap: .55rem; }

/* ⚠️ The character question is a different shape of question and needs a different control.
   Rendered as full-width stacked cards it ran to 27 of them — a mile of scrolling that destroys
   the "three questions, about twenty seconds" promise the homepage makes. Chips that wrap fit the
   same answers into a few rows. Found by looking at the page, not by reading the markup. */
.options--chips { display: flex; flex-wrap: wrap; gap: .4rem; }
.options--chips .opt { flex: 0 0 auto; }
.options--chips label { padding: .5rem .8rem; border-radius: 999px; font-size: .93rem; }

/* Radios are real radios underneath — keyboard and screen readers get the semantics for free. */
.opt { position: relative; }
.opt input {
  position: absolute; opacity: 0; width: 0; height: 0;
}
.opt label {
  display: block; cursor: pointer;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .85rem 1rem;
  font: 500 1rem/1.35 inherit;
  color: var(--ink);
  transition: border-color .12s, background .12s;
}
.opt label small {
  display: block; font-weight: 400; font-size: .86rem; color: var(--mute); margin-top: .18rem;
}
.opt input:checked + label {
  border-color: var(--accent); background: var(--accent-sf);
  /* A second ring rather than a thicker border, so the card does not shift by a pixel when picked. */
  box-shadow: 0 0 0 1px var(--accent);
}
.opt input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) { .opt label:hover { border-color: var(--mute); } }

/* ⚠️ A segmented control, and it is real radios underneath like every other choice here — the
   keyboard and screen-reader semantics come free and cannot drift from what the mouse does.
   It exists because questions 2 and 4 were merged: the two modes are what used to be two separate
   questions that read as the same question. */
.modes {
  display: flex; gap: .25rem; margin: 0 0 .85rem;
  background: var(--sugar); border-radius: 11px; padding: .25rem;
}
.modes .mode { flex: 1 1 0; min-width: 0; position: relative; }
.modes input { position: absolute; opacity: 0; width: 0; height: 0; }
.modes label {
  display: block; cursor: pointer; text-align: center;
  padding: .6rem .5rem; border-radius: 8px;
  font: 500 .93rem/1.3 inherit; color: var(--mute);
  transition: background .12s, color .12s;
}
.modes input:checked + label { background: var(--card); color: var(--ink); font-weight: 600; }
.modes input:focus-visible + label { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- the optional seed ------------------------------------------------------------------- */

/* The character filter shares the seed field's shape on purpose — same job, same look, and one
   place to change the 16px floor below rather than two that can drift apart. */
.char-search { position: relative; margin: 0 0 .7rem; }
.char-search input,
.seed-field input {
  width: 100%;
  /* ⛔ 16px minimum. Anything smaller and iOS zooms the page on tap and never zooms back. */
  font: 400 16px/1.4 inherit;
  padding: .8rem .95rem;
  color: var(--ink); background: var(--page);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.char-search input::placeholder,
.seed-field input::placeholder { font-size: .92rem; color: var(--mute); }
.char-search input:focus,
.seed-field input:focus { outline: none; border-color: var(--accent); }

.seed-results {
  list-style: none; margin: .5rem 0 0; padding: 0; display: grid; gap: .35rem;
}
.seed-results li button {
  width: 100%; text-align: left; cursor: pointer;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  padding: .55rem .8rem;
  font: 400 .95rem/1.3 inherit; color: var(--ink);
}
.seed-results li button b { font-weight: 600; }
.seed-results li button span { color: var(--mute); font-size: .86rem; }

.chosen {
  display: flex; align-items: center; justify-content: space-between; gap: .6rem;
  background: var(--accent-sf); border: 1px solid var(--accent); border-radius: var(--radius);
  padding: .7rem .9rem; font: 500 .96rem/1.3 inherit; color: var(--ink);
}
.chosen button {
  background: none; border: 0; cursor: pointer; color: var(--accent);
  font: 500 .9rem inherit; text-decoration: underline; padding: 0;
}

/* ---- the button -------------------------------------------------------------------------- */

.go {
  width: 100%; margin-top: 1.6rem; cursor: pointer;
  background: var(--accent); color: var(--accent-in);
  border: 0; border-radius: var(--radius); padding: 1.05rem 1.2rem;
  font: 700 1.06rem/1 inherit; letter-spacing: .005em;
  box-shadow: 0 6px 16px -8px var(--accent);
}
.go:disabled { opacity: .45; cursor: default; }
.go:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* ---- results ----------------------------------------------------------------------------- */

/* ⚠️ TWO COLUMNS FROM 46rem, AND THE REASON IS LENGTH RATHER THAN TASTE. A theme page renders 16
   cards; stacked one per row that is a column of wide, half-empty rows sixteen deep, which is what
   the owner meant by "it doesn't look nice" — Waterstones' category pages put the same content in a
   grid and it reads as a shelf instead of a list.
   ⚠️ NOT cover-on-top like theirs, and that is deliberate: their card carries four short lines
   (title, author, format, price) where ours carries a REASON SENTENCE, which is the product. A
   full-width cover above a paragraph makes a 570px card, and sixteen of those is longer than what
   it replaced. Side-by-side keeps the card compact; the grid does the density. */
.books { list-style: none; margin: 1.5rem 0 0; padding: 0; display: grid; gap: 1rem; }
@media (min-width: 46rem) { .books { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.book {
  /* ⚠️ THE COVER IS THE ONLY IMAGERY THIS SITE HAS — no photography, no illustration, one icon —
     and until 2026-09-17 it was shown at 74px. hardcover.app's homepage is a WALL of covers;
     Waterstones runs them at ~110px with a price under each; wedissimo uses photo tiles with a
     count badge where this site uses plain text pills. The one visual asset the project owns was
     being used at the smallest size it could be. */
  display: grid; grid-template-columns: var(--cover-w) 1fr; gap: 1rem;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1rem;
  /* ⚠️ The shadow was added in 2026-08-18 because a white card on a WHITE page has no edge of its
     own and the results read as a flat list. The cream ground of 2026-08-21 separates the cards
     again on its own, so the shadow is now doing less work — kept, but softened and warmed to the
     ink rather than the old blue-black. ⚠️ If the ground ever goes back to white, this has to grow
     again: it is a pair with the page colour, not an independent choice. */
  box-shadow: 0 1px 2px rgba(42, 37, 33, .04), 0 8px 20px -14px rgba(42, 37, 33, .22);
}
.book .cover {
  width: var(--cover-w); aspect-ratio: 3 / 4; border-radius: 4px; overflow: hidden;
  /* An edge, so a cover with a white background reads as an object rather than floating. */
  box-shadow: inset 0 0 0 1px rgba(42, 37, 33, .1);
  background: var(--sugar); display: grid; place-items: center;
  /* ⚠️ position:relative + an absolutely positioned image, NOT two grid items. As grid siblings
     the initial and the cover stacked into two ROWS, so every card showed a stray letter sitting
     above its cover. The letter has to be UNDERNEATH, revealed only when the image fails. */
  position: relative;
}
.book .cover img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
}
/* The typographic fallback for a missing or broken cover. It has to look deliberate: cover_i being
   present does not mean the image is right, and merged records have lottery covers. */
/* ⚠️ THE FALLBACK IS 21% OF THE CORPUS — 627 of 2,991 books have no coverId — so about three of
   the sixteen cards on a theme page are this. It has to read as a plain hardback rather than as a
   missing image, which is what the spine is for. A ::before is painted BEFORE the element's
   positioned descendants, so a real cover hides it with no extra rule. */
.book .cover::before {
  content: ''; position: absolute; inset: 0 auto 0 0; width: 6px;
  background: linear-gradient(90deg, rgba(42, 37, 33, .13), rgba(42, 37, 33, 0));
}
.book .cover .initials {
  font: 600 1.7rem/1 var(--font-display); color: var(--mute);
  padding-left: .35rem;   /* clear of the spine */
}
.book h3 {
  font-size: var(--t-title); margin-bottom: .1rem; letter-spacing: -.01em;
  /* The one surviving serif: a book title should look like a book title. */
  font-family: var(--font-display);
}
.book .by { font: 400 .88rem/1.4 inherit; color: var(--mute); }
.book .why {
  margin: .5rem 0 0; font-size: .97rem; color: var(--body);
}
.book .meta {
  /* ⚠️ Flex rather than an inline text node, and the reason is a widow: the step chip followed by
     "&middot; 10 pages" broke as "· 10" / "pages" at phone width, and the bigger cover makes the
     text column narrower still. The page count is its own element now and cannot split.
     ⚠️ PLAYBOOK 15 warns against flex for an icon-plus-prose row; this is a chip and a label,
     which is what flex is actually for. */
  margin-top: .6rem; font: 400 .82rem/1.4 inherit; color: var(--mute);
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .25rem .55rem;
}
.book .meta .pages { white-space: nowrap; }
/* ⚠️ The rung is the one piece of meaning on the card, so it gets the colour and a chip rather
   than being grey text in a row of grey text. `data-rung` is written by BOTH renderers — the
   static page's app.js and the server-rendered pages.js — and they must agree; there is a test. */
.book .meta .rung {
  font-weight: 700; color: var(--step);
  background: color-mix(in srgb, var(--step) 12%, transparent);
  border-radius: 999px; padding: .2rem .55rem; margin-right: .1rem;
  font-size: .8rem; letter-spacing: .005em;
}
.book .meta .rung[data-step='1'] { --step: var(--step-1); }
.book .meta .rung[data-step='2'] { --step: var(--step-2); }
.book .meta .rung[data-step='3'] { --step: var(--step-3); }
.book .meta .rung[data-step='4'] { --step: var(--step-4); }
.book .meta .rung[data-step='5'] { --step: var(--step-5); }

/* ---- the affiliate disclosure ------------------------------------------------------------- */

/* ⚠️ NOT small print, and resisting the urge to make it small print is the whole design question
   here. Every affiliate site puts this at 11px in grey, which satisfies the letter of "clear and
   conspicuous" and none of its point. It sits at body size on a tinted band directly above the
   books, so it reads as a statement the site is making rather than a disclaimer it is hiding.

   ⚠️ It is also, today, a piece of the ARGUMENT — while nothing is tagged it says the links earn us
   nothing and that no shop pays for a place on the list, which is the site's actual pitch. That is
   why it is not tucked away: it stops being a cost and starts being a reason to trust the page. */
/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   THE AFFILIATE DISCLOSURE — ⚠️ RESTYLED 2026-09-17, AND NOT ONE WORD OF IT CHANGED.
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   ⛔ EVERYTHING THE 19 LEGAL TESTS PIN IS UNTOUCHED, and that is the point of doing this in CSS:
   the wording still comes from src/lib/legal.js and nowhere else, `class="disclosure"` still
   appears before `class="buy"` in the document, it is still once per page, still body size rather
   than small print, still links to /about. None of that was ever a VISUAL constraint.

   ⚠️ WHAT WAS WRONG WAS THE REGISTER. It had a 3px left rule and `border-radius: 0 8px 8px 0` —
   the Bootstrap "alert" pattern, the visual language of caution. The owner, 2026-09-17: "the recent
   disclosure statement makes it even worse looking… must be a nicer way."

   ⭐ AND THE WORDS ARE NOT A WARNING, THEY ARE THE BEST CREDENTIAL ON THE SITE: "These links earn
   us nothing. No shop pays for a place on this list, and when that changes this line will change
   with it." PLAYBOOK ranks self-incriminating disclosure above every trust badge there is. It was
   being formatted as a disclaimer.

   ⛔ AND IT CARRIES NO ICON, WHICH WAS TRIED AND REMOVED THE SAME HOUR. The wordmark's own book
   glyph went in at 1.1rem and rendered as a dark smudge — it is drawn for a 32px favicon with a
   background behind it, and nothing of the two-page shape survives at half that size with none.
   PLAYBOOK 33: "when a design keeps not landing, the thing being added may be the problem —
   propose REMOVING the element rather than refining it." An illegible mark is worse than no mark.
   ⚠️ A tick was the other candidate and is worse for a subtler reason worth keeping: it reads well
   beside "these links earn us nothing" and reads as ticking off a benefit beside the TAGGED
   wording, "we may earn a commission". This block has two states and its furniture must suit both.

   ⚠️ CSS ONLY, deliberately. The markup exists in THREE places — disclosureBlock() in src/pages.js,
   public/app.js for client-rendered results, and the wording in src/lib/legal.js. Styling it here
   changes all three and can drift from none of them. */
/* ⚠️ NO BOX, 2026-09-18. Owner: "i dont like the [disclosure] window so prominent… not in a
   separate window so it doesnt attract so attention."

   ⛔ WHAT COULD NOT CHANGE, AND WHY IT WAS ASKED AND ANSWERED RATHER THAN QUIETLY IGNORED: the
   owner also asked whether it could move elsewhere. It cannot move BELOW the buy links. That is
   the single rule the whole legal layer is built on — nobody may reach a buy link without having
   passed it — and a disclosure underneath the thing it discloses is one a reader meets after
   clicking, which is what "clear and conspicuous" exists to prevent. `test/legal.test.js` asserts
   the document order, not the styling.

   ✅ So it stays exactly where it was and stops looking like an alert: no panel, no border, no
   tint. It now reads as the last line of the page's own introduction, which is what it is.

   ⚠️ THIS SOFTENS A RECORDED DECISION AND THE TRADE IS WORTH KNOWING. CLAUDE.md: "The disclosure
   is body size on a tinted sage band, not small print. Every affiliate site sets this at 11px in
   grey, which satisfies the letter of 'clear and conspicuous' and none of its point." It is .9rem
   in --body here, not 11px in --mute: quieter furniture, still ordinary readable prose in the
   page's own text colour. ⛔ Do not take it further into grey small print — that is the line the
   earlier decision was drawn against, and this is deliberately on the near side of it. */
.disclosure {
  margin: -.3rem 0 1.7rem;
  font-size: .9rem; line-height: 1.6; color: var(--body); max-width: 44rem;
}
.disclosure a {
  color: var(--accent); font-weight: 600;
  text-underline-offset: .18em; text-decoration-thickness: 1px;
}
@media (hover: hover) { .disclosure a:hover { color: var(--ink); } }

/* ---- prose pages: /about and /privacy ------------------------------------------------------ */

/* Narrower than the book pages on purpose — these are read, not scanned, and a legal page set to
   the full grid width is the one nobody finishes. ⚠️ `--measure` rather than a number picked here:
   it is the width the ledes already use, so these pages read at the same line length as the rest of
   the site instead of introducing a second one. */
.prose { max-width: var(--measure); }
.prose section { margin-top: 1.8rem; }
.prose h2 { font-size: var(--t-h3); margin: 0 0 .5rem; }
.prose p { margin: 0 0 .8rem; }

.buy { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .7rem; }
.buy a {
  font: 600 .84rem/1 inherit; text-decoration: none;
  border: 1px solid var(--line); border-radius: 999px; padding: .5rem .8rem;
  color: var(--ink); background: var(--sugar);
}
@media (hover: hover) { .buy a:hover { border-color: var(--accent); color: var(--accent); } }

/* ---- footer ------------------------------------------------------------------------------ */

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   ⚠️ THE ONE DARK AREA ON THE SITE, and it is there for a measured reason rather than a taste one.
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   PLAYBOOK, from the northwoodmetrics "it looks grey and dead" post-mortem — a verdict given TWICE,
   seven days apart, on two different palettes: measure DARK MASS (lightness ≤30%) and COLOURED
   AREA (saturation ≥18%) across every element with an opaque background. Seven credible
   comparators ran 1.8–43.7% dark and 0.4–39.5% coloured. northwoodmetrics ran 0.0% / 0.2%.

   ⛔ MEASURED HERE 2026-09-17, AND IT WAS THE SAME PLACE. Every large background on the site:

       --page / --card   #FFFFFF   lightness 100%   saturation  0%
       --sugar           #F3F1ED   lightness  94%   saturation 20%
       --accent-sf       #F0F3EE   lightness  94%   saturation 17%

   Dark mass 0.0% — no region of any page was darker than 94% lightness. And PLAYBOOK's own rule,
   "if a band is under ~30% saturation it is not a band, it is a stain", condemns both bands.
   ⚠️ The sting: --accent is 12.4% saturated, so the one accent on the page is itself below the
   threshold for counting as coloured.

   ⚠️ AND A CORRECTION WORTH KEEPING, because the first reading of this overstated the case. Run as
   written, the metric scores the homepage at 27.2% COLOURED before any of this — the --sugar bands
   are 20% saturated and clear the 18% bar. So the site was never short of "coloured area" by the
   letter of the measurement. What it was short of is the thing the measurement is a proxy for: a
   band at 94% lightness does not read as a band, which is exactly what PLAYBOOK means by "under
   ~30% saturation it is not a band, it is a stain". ⛔ The unambiguous number was DARK MASS AT 0.0%,
   and that is the one this change fixes. Quote that one.

   ✅ MEASURED AFTER, 2026-09-17 — both pages now sit inside the comparator band (1.8–43.7% dark,
   0.4–39.5% coloured):

       homepage      9.3% dark   27.2% coloured      (was 0.0% dark)
       theme page    6.6% dark    3.1% coloured      (was 0.0% dark)

   ⭐ "Every credible site has mass, or chroma, or both. A page with neither reads as dead whatever
   its hexes are." The two are SUBSTITUTES, not both required — so this buys mass and leaves the
   palette alone. BookTrust (the competitor), Waterstones and needabarista all close on a dark band;
   swimmingpoolsnearme and wedissimo get theirs from photography, which this site does not have.

   ⛔ NO NEW COLOURS. --ink was already a token. The owner approved one dark area on 2026-09-17,
   and the palette decided over three rounds in August is untouched.
   ⛔ AND THIS IS NOT A DARK THEME. See the note at the top of this file: there is no dark mode on
   this site by instruction, `color-scheme: light` stays, and one dark SECTION is not one.
   ═══════════════════════════════════════════════════════════════════════════════════════════════ */
footer {
  margin-top: 4rem;
  background: var(--ink);
  color: var(--on-ink-mute);
  font: 400 var(--t-small)/1.6 var(--font-body);
}
/* ⚠️ .wrap carries `padding-bottom: 4rem` for the page; inside a band that is a hole in the dark. */
footer .wrap { padding-top: 2.8rem; padding-bottom: 2.4rem; }
footer a { color: var(--on-ink); text-decoration-color: var(--on-ink-line); }
@media (hover: hover) { footer a:hover { color: #FFFFFF; text-decoration-color: currentColor; } }

/* ⚠️ Columns rather than three loose sentences. The old footer was a country row, a credit line and
   an email address stacked one under another with nothing to say which was which — and no room for
   the pages this site still owes (a privacy notice, an affiliate disclosure, trader identity: see
   TODO.md). Built as columns now so those are a list item each rather than another redesign. */
.foot-cols { display: grid; gap: 1.6rem; margin-bottom: 1.5rem; }
.foot-what { margin: 0; max-width: 26rem; }
.foot-what strong { color: var(--on-ink); font-weight: 700; }
.foot-h {
  font: 700 var(--t-micro)/1.4 inherit; letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent-lt); margin: 0 0 .6rem;
}
.foot-links { list-style: none; margin: 0; padding: 0; display: grid; gap: .35rem; }
.foot-fine { margin: 1.4rem 0 0; }

/* The country picker. Deliberately plain and small — it is a fallback for a wrong guess, not a
   feature, and anything eye-catching would invite people to fiddle with a setting that is already
   right for almost all of them.
   ⚠️ On /match it is a real <form> with a submit button, because that page ships no JavaScript at
   all; on the homepage app.js reloads on change and there is no button to show. Same rule sizes
   both. */
.market-row {
  display: flex; align-items: center; gap: .45rem; flex-wrap: wrap;
  margin: 0; padding-top: 1.4rem; border-top: 1px solid var(--on-ink-line);
}
/* ⚠️ The label holds a FLAG and nothing else since 2026-08-22 — owner's call, "instead of globe
   have the flag, and then straight the country". It is a <label for="market">, so clicking the
   flag opens the picker too: the flag and the country name are one control, not a decoration
   sitting next to one. */
.market-row label { display: inline-flex; align-items: center; line-height: 0; cursor: pointer; }
.flag { border-radius: 2px; box-shadow: 0 0 0 1px rgba(255, 255, 255, .25); display: block; }

/* Deliberately unadorned: with the flag beside it, a bordered box reads as a form field in a footer
   that has no other form. Just the country name, and the browser's own arrow. */
.market-row select {
  font: inherit; font-size: .92rem;
  color: var(--on-ink); background: transparent;
  border: 0; border-radius: 6px;
  padding: .1rem .15rem;
}
.market-row select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.market-row button {
  font: 600 .8rem inherit; cursor: pointer;
  color: var(--on-ink); background: transparent;
  border: 1px solid var(--on-ink-line); border-radius: 999px; padding: .3rem .7rem;
}
@media (hover: hover) { .market-row button:hover { border-color: var(--accent-lt); color: var(--accent-lt); } }

@media (min-width: 46rem) {
  .foot-cols { grid-template-columns: 1.6fr 1fr 1fr; gap: 2.2rem; }
}

.hidden { display: none !important; }


/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   MOTION — PLAYBOOK mechanic 5: "motion on ENTRY, not on hover… this is the actual source of
   'silky'." Measured here 2026-09-17, before any of this: three `transition` declarations on the
   whole site, no `@keyframes`, no `transform`, and not one `:active` state.
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   ⛔ THE HIDDEN STATE IS GATED ON .js-reveal, WHICH ONLY public/reveal.js CAN SET, and that is the
   whole safety design rather than a detail. No JavaScript, an old browser, a thrown error, or a
   visitor who has asked for reduced motion — every one of those leaves a page where nothing is
   hidden, because the class was never added. PLAYBOOK says it in as many words: wrap it in
   prefers-reduced-motion and MAKE THE REDUCED CASE SHOW EVERYTHING. A page that is permanently
   blank for the people who asked for less motion is the failure mode here. */
.js-reveal .reveal {
  opacity: 0; transform: translateY(16px);
  transition: opacity .42s ease, transform .42s cubic-bezier(.2, .7, .3, 1);
  transition-delay: calc(var(--r, 0) * 45ms);
}
.js-reveal .reveal.is-in { opacity: 1; transform: none; }

/* ⚠️ The belt to reveal.js's braces. reveal.js already returns early on reduced motion, so this
   only catches anything that gains a transition later without thinking about it. */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
  .js-reveal .reveal { opacity: 1 !important; transform: none !important; }
}

/* ⚠️ THE PRESSED STATE, which the site had nowhere. A control that does not acknowledge the tap is
   the difference between a page and an app, and on a phone it is the only feedback there is —
   :hover does not exist. */
.go:active, .nav-cta:active, .market-row button:active, .seed-results li button:active {
  transform: translateY(1px);
}
.opt label:active, .chips a:active, .buy a:active { transform: translateY(1px); }

/* A card lifts rather than merely changing its border. ⛔ The base shadow is load-bearing (white
   card on white ground, playbook principle 31) so this DEEPENS it — it never replaces it. */
@media (hover: hover) {
  .book, .guides li { transition: box-shadow .2s ease, transform .2s ease; }
  .book:hover, .guides li:hover {
    transform: translateY(-2px);
    box-shadow: 0 2px 4px rgba(42, 37, 33, .05), 0 14px 30px -18px rgba(42, 37, 33, .32);
  }
}

/* ── the guides index ──────────────────────────────────────────────────────────────────────────
   ⚠️ Deliberately the SAME card as .book rather than a new treatment. A guide and a book are both
   "a thing you might open next", and giving the guides their own look would make the section read
   as bolted on — which is exactly what an undated blog on a small site usually looks like. */
.guides { list-style: none; margin: 1.75rem 0 0; padding: 0; display: grid; gap: 1rem; }

.guides li {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.1rem 1.25rem;
  box-shadow: 0 1px 2px rgba(42, 37, 33, .04), 0 8px 20px -14px rgba(42, 37, 33, .22);
}

.guides h2 { margin: 0 0 .35rem; font-size: var(--t-h3); line-height: 1.3; }

/* ⚠️ No underline on the card title — the whole card reads as one thing, and an underlined serif
   heading looks like a 1998 link list. The hover state is what says it is clickable. */
.guides h2 a { color: var(--ink); text-decoration: none; }
@media (hover: hover) { .guides h2 a:hover { color: var(--accent); } }

.guides p { margin: 0; color: var(--mute); font-size: .95rem; line-height: 1.5; }

/* A guide's prose. ⚠️ Narrower than the book grid on purpose: a paragraph is read, not scanned,
   and the same measure that suits three-column cards is too wide for continuous text. */
main > section > p { max-width: 34rem; }

/* ── the browse sections ───────────────────────────────────────────────────────────────────────
   ⚠️ CHIPS, NOT A LIST. The first outside user's verdict was "too much of a list", and the answer
   recorded then was fewer things on screen at once rather than a different control. A wrapped row
   of chips reads as one object; twelve <li> bullets read as twelve decisions. */
.browse { margin-top: 3rem; }
/* ⚠️ Was an inline style="margin-top:2.5rem" on two h2s. An inline style is the one thing a
   stylesheet cannot override, so it is the one thing a redesign silently fails to change. */
.browse-next { margin-top: 2.6rem; }
.browse-lede, .guides-teaser .browse-lede { color: var(--mute); max-width: 34rem; margin: .4rem 0 0; }

/* ⚠️ TILES, NOT PILLS, AND THE COUNT IS THE POINT. PLAYBOOK, from the weddingvenuelist review:
   "`Name (49)` is Wikipedia's category convention, character for character. Counts belong
   right-aligned INSIDE a tile." Until 2026-09-17 this section was 32 identical white text pills
   with no numbers on them — a phone book, and the thing that makes an index feel like one.
   ⛔ The numbers are generated by scripts/build-browse.mjs and held against the corpus by a test;
   do not hand-edit them. ⛔ And there are deliberately no cover images here — see the note above
   the markers in public/index.html for the rate limit that forbids them. */
.tiles {
  list-style: none; margin: 1.2rem 0 0; padding: 0;
  display: grid; gap: .5rem;
  grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
}
.tiles a {
  display: flex; align-items: baseline; justify-content: space-between; gap: .6rem;
  padding: .72rem .9rem;
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  color: var(--ink); text-decoration: none; font-size: .95rem; line-height: 1.3;
  transition: border-color .12s, box-shadow .2s ease, transform .2s ease;
}
.tile-n { font-weight: 600; min-width: 0; }
.tile-c { color: var(--mute); font-size: .8rem; font-variant-numeric: tabular-nums; flex: 0 0 auto; }
@media (hover: hover) {
  .tiles a:hover {
    border-color: var(--accent); transform: translateY(-1px);
    box-shadow: 0 8px 18px -12px rgba(42, 37, 33, .35);
  }
  .tiles a:hover .tile-c { color: var(--accent); }
}
.tiles a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Visually hidden, still read aloud. */
.sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.chips { list-style: none; display: flex; flex-wrap: wrap; gap: .5rem; margin: 1.1rem 0 0; padding: 0; }

.chips a {
  display: inline-block; padding: .44rem .8rem;
  background: var(--card); border: 1px solid var(--line); border-radius: 999px;
  color: var(--ink); text-decoration: none; font-size: .92rem; line-height: 1.35;
}
@media (hover: hover) { .chips a:hover { border-color: var(--accent); color: var(--accent); } }

.guides-teaser { margin-top: 3rem; }

/* ── where the books come from ───────────────────────────────────────────────────────────────── */
.where-list { list-style: none; margin: 1.5rem 0 0; padding: 0; display: grid; gap: 1.5rem; }
.where-list h3 { margin: 0 0 .3rem; font-size: var(--t-label); }
.where-list p { margin: 0; color: var(--mute); font-size: .95rem; line-height: 1.55; }
@media (min-width: 46rem) { .where-list { grid-template-columns: repeat(3, 1fr); gap: 2rem; } }

/* ── the questions ─────────────────────────────────────────────────────────────────────────────
   ⚠️ A <dl>, not a set of collapsed <details>. An accordion hides the answers from a reader AND
   makes the page look emptier, which is the opposite of what this section is here to fix. */
.faq { margin: 3rem 0 1rem; }
.faq dl { margin: 1.4rem 0 0; max-width: 38rem; }
.faq dt { font-weight: 600; margin-top: 1.3rem; }
.faq dt:first-child { margin-top: 0; }
.faq dd { margin: .35rem 0 0; color: var(--mute); line-height: 1.6; }

/* ⚠️ Was an inline style="margin-top:2rem" on five closing links in src/pages.js. An inline style
   is the one thing a stylesheet cannot override, which makes it the one thing a redesign silently
   fails to change. */
.page-end { margin-top: 2rem; }

/* ⚠️ BREADCRUMBS — added 2026-09-17. A theme page had no way back up: no crumbs, no link to the
   full list, one closing link to the homepage. PLAYBOOK's index warning is the same one — "having
   ONE DOOR is what makes an index feel like a phone book, more than its length is."
   ⛔ Drawn AND marked up: the BreadcrumbList JSON-LD in src/pages.js is what puts the trail under
   the result in Google, which is half the point of having one. */
.crumbs {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: .1rem .45rem;
  margin: 0 0 .9rem; font: 500 .82rem/1.4 var(--font-body); color: var(--mute);
}
.crumbs a { color: var(--mute); text-decoration: none; }
@media (hover: hover) { .crumbs a:hover { color: var(--accent); text-decoration: underline; } }
.crumbs a + a::before, .crumbs a + span::before {
  content: '›'; margin-right: .45rem; color: var(--line);
}
.crumbs [aria-current] { color: var(--ink); }

.more-themes { margin-top: 3rem; }

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   THE HOMEPAGE SHELVES — four theme pages, chosen by today's date. src/lib/shelves.js.
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   ⚠️ This is the answer to "the browse is so empty" and "we need more, much more". The homepage
   carried 39 identical text pills; Waterstones' homepage is shelves of covers and nothing else,
   and the covers are the only imagery this project owns.

   ⛔ THE WHOLE SHELF IS ONE LINK. Six anchors with no distinct text is six identical
   announcements to a screen reader, and a parent clicking a cover expects THAT book — per-book
   pages are deliberately out of scope, so one target stated once is the honest version. The covers
   are aria-hidden and alt="": they are a picture of a shelf, not six things to choose between. */
.shelves { margin-top: 3rem; }

.shelf-grid {
  display: grid; gap: .9rem; margin-top: 1.3rem;
  grid-template-columns: 1fr;
}
@media (min-width: 46rem) { .shelf-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.shelf {
  display: block; text-decoration: none; color: inherit;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.1rem 1.15rem 1rem;
  box-shadow: 0 1px 2px rgba(42, 37, 33, .04), 0 8px 20px -14px rgba(42, 37, 33, .22);
  transition: box-shadow .2s ease, transform .2s ease, border-color .12s;
}
@media (hover: hover) {
  .shelf:hover {
    transform: translateY(-2px); border-color: var(--accent);
    box-shadow: 0 2px 4px rgba(42, 37, 33, .05), 0 16px 32px -18px rgba(42, 37, 33, .34);
  }
  .shelf:hover .shelf-name { color: var(--accent); }
}
.shelf:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.shelf:active { transform: translateY(0); }

/* ⚠️ OVERLAPPED, because a row of evenly spaced thumbnails reads as a grid and books on a shelf
   lean. The shadow is what makes the overlap legible — without it they merge into one strip. */
.shelf-covers {
  display: flex; align-items: flex-end; margin-bottom: .85rem; padding-left: 2px;
}
.shelf-covers img {
  /* ⚠️ flex-basis 0 rather than a fixed width, so the stack FILLS the card at every breakpoint.
     Fixed-width covers left a third of each card empty on desktop, which read as a card that had
     failed to load rather than as a deliberate stack. */
  flex: 1 1 0; min-width: 0; max-width: 5.4rem;
  aspect-ratio: 3 / 4; object-fit: cover; display: block;
  border-radius: 3px; background: var(--sugar);
  box-shadow: 0 1px 2px rgba(42, 37, 33, .08), 0 6px 14px -8px rgba(42, 37, 33, .45);
  margin-left: -.85rem;
  transition: transform .2s ease;
}
.shelf-covers img:first-child { margin-left: 0; }
/* The stack fans out a little on hover — the one place this site earns a flourish. */
@media (hover: hover) {
  .shelf:hover .shelf-covers img { margin-left: -.55rem; }
  .shelf:hover .shelf-covers img:first-child { margin-left: 0; }
}

.shelf-name {
  display: block; font: 700 var(--t-h3)/1.25 var(--font-body); color: var(--ink);
  letter-spacing: -.01em; transition: color .12s;
}
.shelf-meta {
  display: block; margin-top: .15rem;
  font: 400 var(--t-small)/1.4 var(--font-body); color: var(--mute);
  font-variant-numeric: tabular-nums;
}
.shelf-all { margin: 1.3rem 0 0; font-size: var(--t-small); }

/* ═══════════════════════════════════════════════════════════════════════════════════════════════
   /browse — every shelf, under the axis a parent would pick from.
   ═══════════════════════════════════════════════════════════════════════════════════════════════
   ⚠️ Cover-led, which round 1 wrongly ruled out over a rate limit that does not apply to CoverID
   requests. Three covers a tile rather than the shelves' five: 39 tiles is a lot of images, and
   every one is lazy so a visitor only fetches the rows they reach. */
.bgroup { margin-top: 2.8rem; }
.btiles {
  list-style: none; margin: 1.2rem 0 0; padding: 0;
  display: grid; gap: .8rem;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
}
.btile {
  display: block; text-decoration: none; color: inherit;
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  padding: .8rem .85rem .7rem;
  transition: box-shadow .2s ease, transform .2s ease, border-color .12s;
}
@media (hover: hover) {
  .btile:hover {
    transform: translateY(-2px); border-color: var(--accent);
    box-shadow: 0 10px 22px -14px rgba(42, 37, 33, .38);
  }
  .btile:hover .btile-name { color: var(--accent); }
}
.btile:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.btile:active { transform: translateY(0); }

.btile-covers { display: flex; align-items: flex-end; margin-bottom: .6rem; }
.btile-covers img {
  flex: 1 1 0; min-width: 0; aspect-ratio: 3 / 4; object-fit: cover; display: block;
  border-radius: 3px; background: var(--sugar);
  box-shadow: 0 1px 2px rgba(42, 37, 33, .08), 0 5px 12px -7px rgba(42, 37, 33, .45);
  margin-left: -.7rem;
}
.btile-covers img:first-child { margin-left: 0; }

.btile-name {
  display: block; font: 600 .96rem/1.25 var(--font-body); color: var(--ink);
  transition: color .12s;
}
.btile-count {
  display: block; margin-top: .1rem;
  font: 400 var(--t-micro)/1.4 var(--font-body); color: var(--mute);
  font-variant-numeric: tabular-nums;
}

/* ⚠️ The scale claim, reduced from a three-column band to one line. PLAYBOOK: "nobody leads with
   the dataset; everybody leads with the job to be done — the scale claim goes underneath as
   reassurance, never as the headline." */
.trust {
  margin: 2.2rem 0 0; max-width: 40rem;
  font-size: var(--t-small); line-height: 1.6; color: var(--mute);
}
.trust a { color: var(--body); }
@media (hover: hover) { .trust a:hover { color: var(--accent); } }

/* ⚠️ THE TWO CONTROLS UNDER THE RESULTS, SAME SIZE, SIDE BY SIDE — owner, 2026-09-18. They were
   not even the same KIND of control before: "Start again" was a plain text link on /match and a
   full sage button in the client-rendered results. That is the two-HTML-sources drift showing up
   in the interface rather than in the furniture, which is why both renderers now emit this markup.
   ⚠️ flex is right here — two buttons is what it is for, not the icon-plus-prose row PLAYBOOK 15
   warns about. `flex: 1 1 0` is what makes them equal regardless of their label lengths. */
.actions {
  display: flex; gap: .6rem; margin-top: 1.9rem; flex-wrap: wrap;
}
.actions .go {
  /* ⚠️ min-width 7rem, not 11 — at 375px two 11rem buttons cannot share a row and wrapped to a
     stack, which is the one thing the owner asked for instead of. Measured, not guessed.
     ⚠️ And it is why the label is "Five more" rather than "Show me five more". Measured at 375px:
     the long label wrapped to two lines inside a 156px button, and "Five more books" still did.
     A button whose text wraps is worse than a short label, and the heading above it already says
     "Five more", so the context carries the rest. `white-space: nowrap` makes a future long label
     overflow visibly rather than wrap quietly. */
  flex: 1 1 0; min-width: 7rem;
  margin-top: 0; text-align: center; text-decoration: none;
  display: flex; align-items: center; justify-content: center;
  /* ⚠️ A TRANSPARENT BORDER ON THE FILLED ONE. .go has `border: 0` and .go--quiet has a 1px
     border, which made the pair 156px and 158px — "same size" is what was asked for, and two
     pixels is enough to see when they sit side by side. */
  border: 1px solid transparent;
  white-space: nowrap;
}
/* The secondary of the pair: same size and shape, no fill, so the primary still reads as primary. */
/* ⚠️ `.actions .go--quiet`, NOT `.go--quiet`. The transparent border above is `.actions .go`,
   which is (0,2,0) and beat a bare class selector — the computed border came back
   `rgba(0, 0, 0, 0)` and the button rendered as plain text next to a filled one. Caught by reading
   the computed style; the screenshot alone just looked a bit subtle. */
.actions .go--quiet {
  background: transparent; color: var(--body);
  /* ⚠️ NOT --line. The hairline that reads correctly around a card is too faint around a control
     this size — at 375px it made "Start again" look like text sitting next to a button rather than
     the matched pair that was asked for. */
  border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
  box-shadow: none;
}
@media (hover: hover) {
  .actions .go--quiet:hover { border-color: var(--accent); color: var(--accent); }
}
.more--done {
  margin: 1rem 0 0; max-width: 34rem;
  font-size: var(--t-small); line-height: 1.6; color: var(--mute);
}
