/* The fairy-book template's design system.
 *
 * Structure lives here; a book's palette recolours only the identity layer, and
 * arrives as custom properties on :root (light) with the same tokens' dark
 * variants swapped in under prefers-color-scheme. Every colour below is a token,
 * so a book that declares a new palette needs no CSS at all.
 *
 * Dark mode is a first-class mode, not an inversion: period line art sits on a
 * paper-tone card in BOTH modes, so dark mode dims the frame and never the
 * engraving. */

:root {
  --measure: 34rem;
  --serif: Iowan Old Style, Palatino Linotype, Palatino, Georgia, serif;
  --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
  --bg: var(--paper);
  --fg: var(--ink);
  --muted: color-mix(in srgb, var(--ink) 62%, var(--paper));

  /* THE CHROME IS NEUTRAL, ON EVERY PAGE, WHATEVER BOOK IT IS.
   *
   * Every other edition of these books falls into the same trap: the volume's
   * colour spreads from the cover onto the header, the links, the rules, and in
   * the worst cases the page ground, and the result is a red website rather than
   * a website showing a red book. This template was doing the first half of it.
   * Used once, the colour means something.
   *
   * So navigation, borders and hover states resolve to the --ui-* scale, which
   * is the same for every page. --tinted still carries the book's ink and now
   * has exactly one job: the drop cap that opens the text. */
  --edge: var(--ui-rule);
  --card: var(--ui-wash);
  --nav-ink: var(--ui-tint);
  --tinted: var(--tint);
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: var(--night);
    --fg: var(--moon);
    --muted: color-mix(in srgb, var(--moon) 62%, var(--night));
    --edge: var(--ui-rule-dark);
    --card: var(--ui-wash-dark);
    --nav-ink: var(--ui-tint-dark);
    --tinted: var(--tint-dark);
  }
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 1.0625rem/1.65 var(--serif);
}
a { color: var(--nav-ink); text-underline-offset: 0.15em; }
a:focus-visible, button:focus-visible, input:focus-visible {
  outline: 3px solid var(--nav-ink); outline-offset: 2px;
}
.skip { position: absolute; left: -9999px; }
.skip:focus { left: 1rem; top: 1rem; background: var(--bg); padding: .5rem 1rem; z-index: 9; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

header.site {
  display: flex; flex-wrap: nowrap; gap: 1rem; align-items: center;
  padding: 1rem clamp(1rem, 4vw, 3rem);
  border-bottom: 1px solid var(--edge);
  font-family: var(--sans); font-size: .9375rem;
  position: sticky; top: 0; z-index: 20; background: var(--bg);
}
/* Not a footer so much as a copyright line: the small print a book carries on
   its copyright page, set once at the foot of the page. No rule above it, no
   columns and no heading, because there is nothing here to navigate. The one
   link is to the publisher, and About when the series has written one. */
footer.site {
  padding: 0 clamp(1rem, 4vw, 3rem) clamp(2rem, 5vw, 3rem);
  margin-top: clamp(3rem, 8vw, 5rem);
  font-family: var(--sans); font-size: .8125rem; color: var(--muted);
}
.colophon, .colophon-more { margin: 0; max-width: var(--measure); }
.colophon-more { margin-top: .35rem; }
footer.site a { color: inherit; text-decoration-color: var(--edge); }
footer.site a:hover { color: var(--fg); text-decoration-color: currentColor; }
/* Padded to a real tap target. Setting line-height on it shrank the box to 19px
   tall, which the mobile harness caught: the text was the right size and the
   thing you press was not. */
/* The name shrinks; it never wraps the bar. `nowrap` on the header plus a
   shrinkable brand is what makes the bar one row at EVERY width instead of at
   the widths this series' title happens to fit: the first version set the
   breakpoint from a measurement of "Andrew Lang: The Coloured Fairy Book
   Series" and would have gone back to a two-row header the day a longer title
   arrived. The ellipsis is the last resort and does not fire here at any width
   the links are shown at. */
.brand { font-weight: 600; text-decoration: none; letter-spacing: .01em; line-height: 1.25;
  display: none; padding: .4rem 0;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (min-width: 46rem) {
  .brand { display: inline-block; }
}

/* --- the drawer: the whole shelf, from anywhere
 *
 * A <details>, not a scripted menu. It opens, closes and answers the keyboard
 * with no JavaScript, which is the line this engine already holds elsewhere:
 * the random link resolves to a real page with scripting off, and a drawer that
 * needed a bundle to open would be the one control that did not. reader.js adds
 * Escape and click-outside on top; neither is needed to use it. */
.drawer { position: relative; }
.drawer-toggle {
  list-style: none; cursor: pointer; display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem; margin: -.5rem 0 -.5rem -.6rem;
  border-radius: 6px;
}
.drawer-toggle::-webkit-details-marker { display: none; }
.drawer-toggle:hover { background: var(--card); }
.drawer-toggle:focus-visible { outline: 3px solid var(--nav-ink); outline-offset: 2px; }
/* Three bars from one element: the box is the middle, the pseudo-elements are
   the outer two. */
.bars, .bars::before, .bars::after {
  display: block; width: 1.25rem; height: 2px; background: currentColor;
  border-radius: 2px; content: "";
}
.bars { position: relative; }
.bars::before { position: absolute; top: -6px; }
.bars::after  { position: absolute; top: 6px; }
.drawer[open] .bars { background: transparent; }
.drawer[open] .bars::before { top: 0; transform: rotate(45deg); }
.drawer[open] .bars::after  { top: 0; transform: rotate(-45deg); }

.drawer-panel {
  position: fixed; inset: 0 auto 0 0; width: min(20rem, 86vw); z-index: 30;
  background: var(--bg); border-right: 1px solid var(--edge);
  box-shadow: 0 0 40px rgba(0,0,0,.35);
  overflow-y: auto; overscroll-behavior: contain;
  /* Top padding clears the toggle, which is lifted over the panel below so it
     can still be pressed. Without both, the panel covered its own close button
     and the drawer could only be shut by Escape or a click outside: two things
     the script adds, so with scripting off it could not be closed at all. */
  padding: 4.25rem 1rem 3rem;
}
.drawer[open] .drawer-toggle { position: relative; z-index: 31; }
.drawer-nav { display: block; }
.drawer-top {
  display: block; padding: .6rem .5rem; text-decoration: none; color: inherit;
  border-radius: 4px; font-weight: 600;
}
.drawer-top:hover { background: var(--card); }
.drawer-book { border-top: 1px solid var(--edge); }
.drawer-book > summary {
  cursor: pointer; padding: .7rem .5rem; list-style: none;
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
}
.drawer-book > summary::-webkit-details-marker { display: none; }
.drawer-book > summary::after {
  content: "\203a"; transform: rotate(90deg); color: var(--muted);
  transition: transform .12s ease;
}
.drawer-book[open] > summary::after { transform: rotate(-90deg); }
.drawer-book > summary:hover { background: var(--card); }
.drawer-book ul { list-style: none; margin: 0 0 .5rem; padding: 0 0 0 .5rem; }
.drawer-book li a {
  display: block; padding: .5rem .5rem; text-decoration: none; color: var(--muted);
  border-radius: 4px; font-size: .9375rem;
}
.drawer-book li a:hover { background: var(--card); color: var(--fg); }
.drawer-hub a { color: var(--nav-ink); }

/* On a phone the bar is two things on one line: the hamburger and the search.
   The drawer already holds the index, the tags and Pick Random, and the search
   placeholder carries the site's name, so the brand would be the name a second
   time on a 390px row.

   `flex: 1` and not `width: 100%` is the whole of what puts the search on the
   toggle's line: full width made the nav wider than the row's remainder, so it
   wrapped to a second row and took the search with it. `min-width: 0` on both
   lets it shrink below the input's intrinsic floor instead of overflowing a
   narrow phone. `margin-left: auto` is for the wide layout, where the nav sits
   opposite the brand. */
.site-nav { display: flex; gap: 1rem; align-items: center; flex: 1; min-width: 0; }
.site-nav > a { display: none; }
.site-nav .search { flex: 1; min-width: 0; }
.site-nav .search input { width: 100%; }
/* WHAT GIVES WAY FIRST, stated rather than left to flexbox's proportional
   default. Above the breakpoint the bar holds a long name and a wide search box
   and there are widths where both do not fit. The search shrinks: it is a hint,
   it degrades to "Search Lang's Fairy B", and it has a floor. The name does not
   shrink until the search has hit that floor, because a site title cut mid-word
   reads as broken in a way a shortened placeholder does not.
   `flex-shrink: 100` against the brand's 1 is what orders that; the input's
   `min-width` is what stops it. Measured after the input started sizing itself
   from the placeholder, which widened it by 27px and clipped the name at 736,
   768 and 1024 while the sweep that would have caught it had already been run. */
@media (min-width: 46rem) {
  /* The weights have to be set at BOTH levels. With the nav on the default
     shrink the header split the deficit in proportion to base widths and handed
     60% of it to the brand, so the search inside never saw any and the name was
     cut anyway. `min-width: auto` restores the automatic minimum the base rule
     turns off for the phone row, which is what stops the nav shrinking past its
     own children. */
  .site-nav { margin-left: auto; flex: 0 100 auto; flex-wrap: nowrap; min-width: auto; }
  /* `flex: 0 100 auto` is grow 0, SHRINK 100. Written `100 1` first, which is
     grow 100 and shrink 1, so the search never gave anything up and the name was
     still the thing that got cut. */
  /* THE FLOOR GOES ON THE FORM, not on the input. An input's automatic minimum
     is its `size` width, so `min-width: auto` on the form floored the search at
     its full 263px and it never gave up a pixel: the name was cut while the box
     beside it was at its preferred width. The form carries the 12rem floor and
     the input simply fills it. */
  .site-nav .search { flex: 0 100 auto; min-width: 12rem; }
  .site-nav .search input { width: 100%; min-width: 0; }
  .brand { flex: 0 1 auto; }
}
/* The inline links come back later than the brand does. Between the two
   breakpoints the bar is the name and the search, and the drawer carries the
   links, which it does at every width anyway.

   `flex: none` on them, because they are the one thing here that must not
   give up a pixel. They sit at their max-content width, so the 0.08px each
   was losing to the search's shrink was enough to break "All stories" onto
   two lines: the header went from 69px to 83px at 1024 and 1032 only, with no
   link visibly narrower, which is what a 0.08px cause looks like from outside. */
@media (min-width: 64rem) {
  .site-nav > a { display: inline; flex: none; white-space: nowrap; }
}
.search { position: relative; }
.search input {
  font: inherit; padding: .3rem .6rem; border: 1px solid var(--edge);
  border-radius: 999px; background: var(--bg); color: var(--fg); min-width: 12rem;
}
#results {
  position: absolute; right: 0; top: 2.2rem; z-index: 5; margin: 0; padding: .25rem;
  list-style: none; background: var(--bg); border: 1px solid var(--edge);
  border-radius: .5rem; max-width: 22rem; box-shadow: 0 6px 24px rgba(0,0,0,.12);
}
#results li a { display: block; padding: .35rem .6rem; text-decoration: none; }
#results li a:hover { background: var(--card); }

main { padding: clamp(1.5rem, 5vw, 4rem) clamp(1rem, 4vw, 3rem); }
h1, h2, h3 { line-height: 1.2; font-weight: 600; }
.hub h1, .unit h1, .page h1 { max-width: var(--measure); }
.shelf-lede { color: var(--muted); max-width: var(--measure); font-size: 1.0625rem;
  margin: 0 0 1.5rem; }
.standfirst { color: var(--muted); max-width: var(--measure); }

/* --- the shelf: the rainbow
 *
 * TWO COLUMNS from the narrowest screen up, not one. Plain
 * `minmax(13rem, 1fr)` needs 26rem plus the gap before a second card fits, so
 * every phone got one book per screen: a 2:3 cover at full width, then a
 * scroll, then the next one. A shelf that shows one book is not a shelf, and
 * the whole point of this series is seeing the colours side by side.
 *
 * ONE RULE, NO BREAKPOINT, and the breakpoint is why. The first version was an
 * explicit 2 up to 30rem and auto-fill above it, which opened a dead zone at
 * exactly the handover: at 480px `minmax(13rem, 1fr)` fits one track and the
 * explicit rule has just switched off, so the shelf dropped back to a single
 * column at the one width the fix was supposed to cover. Measured across nine
 * widths; 480px was the only one wrong, which is how a spot check at 390px
 * passed it.
 *
 * `min(13rem, calc(50% - .5rem))` says: a track is 13rem, but never more than
 * half a row. Below 26rem the half wins and there are exactly two; above it the
 * 13rem wins and auto-fill packs as many as fit. Two is the floor at every
 * width and there is no handover to get wrong. */
.shelf {
  list-style: none; padding: 0; display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(min(13rem, calc(50% - .5rem)), 1fr));
}
@media (min-width: 40rem) { .shelf { gap: 2rem; } }
.shelf-card a { text-decoration: none; color: inherit; display: block; }
.shelf-name { display: block; margin-top: .75rem; font-weight: 600; }
.shelf-blurb { color: var(--muted); font-size: .9375rem; }
.cover {
  display: grid; align-content: center; gap: .75rem; aspect-ratio: 2 / 3;
  width: 100%; border-radius: 3px; padding: 1.25rem; text-align: center;
  background: var(--accent); color: var(--on-accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--deep) 55%, transparent),
              0 2px 10px rgba(0,0,0,.18);
}
img.cover { object-fit: cover; padding: 0; }
.cover-title { font-size: 1.35rem; line-height: 1.15; }
.cover-year { font-family: var(--sans); font-size: .8125rem; letter-spacing: .12em; }
.cover-rule { height: 1px; background: currentColor; opacity: .55; }

/* --- a book hub
 *
 * The head is a TIGHT stack. It was a 1rem grid gap around a 12rem cover with an
 * empty standfirst paragraph inside it, which read as a cover marooned in the
 * middle of the page. The renderer stopped emitting the empty paragraph; this
 * closes the rest, and pulls the cover in so the title sits with it rather than
 * a screen below it. */
.hub-head { display: grid; gap: .5rem; max-width: 46rem; justify-items: start; }
.hub-head .cover { max-width: 9rem; margin-bottom: .25rem; }
.hub-head h1 { margin: 0; }
.hub-head .standfirst, .hub-head .title-page { margin: 0; }
.title-page { font-style: italic; color: var(--muted); }

/* The contents list.
 *
 * Was a browser-numbered <ol> of bare links, each with the story's full
 * bibliographic source underneath: thirty-seven underlined lines interleaved
 * with thirty-seven citations, which is a bibliography, not a table of contents.
 * The credit moved to the story page (where it already was) and the number
 * became an authored element so it can hold its own gutter.
 *
 * Rows rather than list items: a hairline between entries, the number set in the
 * book's own colour, and no underline until hover or focus, so the eye reads a
 * column of TITLES instead of a column of links. */
.toc { max-width: var(--measure); padding-left: 0; list-style: none; margin: 1rem 0 0; }
.toc li { margin: 0; border-top: 1px solid var(--edge); }
.toc li:last-child { border-bottom: 1px solid var(--edge); }
.toc li a {
  display: grid; grid-template-columns: 2.25rem 1fr; align-items: baseline;
  gap: .5rem; padding: .7rem .25rem; text-decoration: none; color: inherit;
}
.toc-n {
  font-family: var(--sans); font-size: .8125rem; font-variant-numeric: tabular-nums;
  color: var(--nav-ink); text-align: right; padding-top: .15em;
}
.toc-t { text-decoration: underline; text-decoration-color: transparent;
  text-underline-offset: .15em; transition: text-decoration-color .12s ease; }
.toc li a:hover .toc-t, .toc li a:focus-visible .toc-t { text-decoration-color: currentColor; }
.toc li a:hover { background: var(--card); }

.in-book, .matter-role {
  display: block; font-family: var(--sans); font-size: .8125rem; color: var(--muted);
}
.matter { max-width: var(--measure); }
.matter-role { text-transform: uppercase; letter-spacing: .1em; }

/* --- the spine: the book's colour, down the page
 *
 * Only on a page that belongs to ONE book (body.has-accent). The shelf and the
 * indexes belong to all of them and get none.
 *
 * Fixed rather than a border on the article, so it runs the whole height of the
 * viewport and stays there through a long tale, which is what "all the way down
 * the page" means for a story that is nine screens long. At left: 0 with no
 * width contribution it cannot cause horizontal overflow, and the page's own
 * padding (min 1rem) keeps text well clear of it. */
body.has-accent::before {
  content: ""; position: fixed; left: 0; top: 0; bottom: 0; width: 7px;
  /* --accent: the book's OFFICIAL colour, the one on its cover, not a derived
     one. The version before this used --tinted, the per-mode readable
     derivative, because at 5px the real navy was hard to see on the night
     ground. That solved the wrong problem. A stripe that is too quiet wants
     more WIDTH; changing its colour instead trades away the only thing the
     stripe is for, which is being that book's red or that book's blue. */
  background: var(--accent); z-index: 10; pointer-events: none;
}

/* --- a unit */
.unit { max-width: var(--measure); }
.breadcrumb { font-family: var(--sans); font-size: .875rem; margin: 0 0 .5rem; }
.unit .credit { color: var(--muted); font-style: italic; margin: .35rem 0 0; }

/* The title block: a small cover, then the title. The cover is what tells a
   reader which of the coloured books they are in without making them read the
   breadcrumb. */
/* The gap below matters as much as the one beside. The cover is taller than the
   title and credit stacked next to it, so its bottom edge sat almost against the
   drop cap and the story appeared to start inside the masthead. */
.unit-title { display: grid; grid-template-columns: 3.25rem 1fr; gap: .9rem;
  align-items: start; margin-bottom: 2rem; }
.unit-title h1 { margin: 0; }
.unit-cover .cover {
  width: 3.25rem; padding: .3rem; gap: .2rem; border-radius: 2px;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--deep) 55%, transparent),
              0 1px 4px rgba(0,0,0,.22);
}
/* The composed cover's lettering does not survive being shrunk to a thumbnail:
   at this size it is a coloured spine, so the type comes out and the rules
   stay. */
.unit-cover .cover-title, .unit-cover .cover-year { display: none; }
.unit-cover .cover-rule { opacity: .8; }
.unit-cover img.cover { height: auto; aspect-ratio: 2 / 3; }
/* Not on a poem: a drop cap belongs to the opening of prose, and :first-of-type
   would otherwise hand one to a tale that opens on a rhyme. */
.text > p:not(.verse):first-of-type::first-letter {
  float: left; font-size: 3.1em; line-height: .82; padding: .06em .08em 0 0;
  color: var(--tinted);
}
.text p { margin: 0 0 1.15em; }

/* --- verse. The line is the unit, and the indent is meaning: a ballad's
   alternating indent is how the printed page says it is a ballad. */
.text p.verse { margin: 1.6em 0; font-style: italic; }
/* The <br>s are the no-CSS fallback; with the stylesheet the spans are already
   blocks and the breaks would double. */
.verse br { display: none; }
.verse .vl { display: block; text-indent: -1.4em; padding-left: 1.4em; }
.verse .vs { display: block; height: .8em; }
.verse .vl-1 { padding-left: 2.8em; }
.verse .vl-2 { padding-left: 4.2em; }
.verse .vl-3 { padding-left: 5.6em; }
.verse .vl-4 { padding-left: 7em; }
.editions { display: flex; gap: .5rem; font-family: var(--sans); font-size: .875rem; margin: 1rem 0 2rem; }
.edition { padding: .25rem .7rem; border: 1px solid var(--edge); border-radius: 999px; text-decoration: none; }
.edition.current { background: var(--ui-accent); color: var(--ui-on-accent); border-color: transparent; }

.plate { margin: 2rem 0; padding: .75rem; background: var(--paper); border-radius: 2px;
  box-shadow: 0 1px 6px rgba(0,0,0,.2); }
/* A vignette is the small decorative cut, not a full-page illustration, and it
   sits inside the measure rather than filling it. A hero opens the page. */
.plate-vignette { max-width: 16rem; margin-inline: auto; }
.plate-hero { margin-top: 0; }
.plate img { width: 100%; height: auto; display: block; }
.plate figcaption { font-family: var(--sans); font-size: .8125rem; color: #55504a; margin-top: .5rem; }
.plate .credit { display: block; opacity: .8; }

/* A DECLARED PLATE WITH NO IMAGE YET. The text knows where every illustration
   goes and what it shows; the files arrive separately and later. So this is the
   normal state of a freshly ingested volume, not a broken one, and it reads as
   a place held open: the printed caption, on the paper, in the plate's own
   slot. It gets no drop shadow, because nothing is sitting on the page yet. */
.plate-stub { box-shadow: none; border: 1px dashed var(--edge); background: none;
  display: flex; align-items: center; justify-content: center; min-height: 5rem;
  padding: 1.25rem; }
.plate-stub figcaption { margin-top: 0; text-align: center; font-style: italic;
  max-width: 28rem; }

/* ALSO TOLD AS: the same tale, in another country and another volume. This is
   what a tale-type number is for, and the number itself stays a quiet footnote
   under the list rather than a heading, because a reader wants the tales. */
.also { margin-top: 3rem; border-top: 1px solid var(--edge); max-width: var(--measure); }
.also h2 { font-family: var(--sans); font-size: .8125rem; text-transform: uppercase;
  letter-spacing: .1em; color: var(--muted); font-weight: 600; }
.also-list { list-style: none; padding: 0; margin: .5rem 0 0; }
.also-list li { border-top: 1px solid var(--edge); }
.also-list li:last-child { border-bottom: 1px solid var(--edge); }
.also-list a { display: block; padding: .7rem .25rem; text-decoration: none; color: inherit; }
.also-list a:hover { background: var(--card); }
.also-type { font-family: var(--sans); font-size: .8125rem; color: var(--muted); }

.footnotes { margin-top: 3rem; border-top: 1px solid var(--edge); font-size: .9375rem; }
.footnotes-head { font-family: var(--sans); font-size: .8125rem; text-transform: uppercase; letter-spacing: .1em; }
.fn-ref a { text-decoration: none; padding: 0 .15em; }
.fn-back { text-decoration: none; }

.commentary {
  margin-top: 3rem; padding: 1.25rem 1.5rem; background: var(--card);
  border-left: 3px solid var(--accent); border-radius: 0 4px 4px 0;
}
.commentary h1, .commentary h2 { font-family: var(--sans); font-size: 1rem;
  text-transform: uppercase; letter-spacing: .08em; margin-top: 0; }
.colophon-block { margin-top: 4rem; font-size: .875rem; color: var(--muted); max-width: var(--measure); }

.tags { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 2.5rem; }
.tag { font-family: var(--sans); font-size: .8125rem; padding: .2rem .6rem;
  border: 1px solid var(--edge); border-radius: 999px; text-decoration: none; }
/* The pager: three fixed cells, so backwards is always on the left and forwards
 * is always on the right whether or not either exists. A flex row that dropped
 * an absent cell used to slide the remaining ones sideways, which is how "next"
 * ended up wherever there was room. */
.unit-nav {
  display: grid; grid-template-columns: 1fr auto 1fr; gap: .75rem;
  margin-top: 3rem; padding-top: 1.25rem; border-top: 1px solid var(--edge);
  font-family: var(--sans); align-items: start;
}
.pager { display: flex; flex-direction: column; gap: .15rem; text-decoration: none;
  color: inherit; padding: .5rem .25rem; border-radius: 4px; min-width: 0; }
.pager.empty { padding: 0; }
.pager.prev { text-align: left; }
.pager.random { text-align: center; align-items: center; }
.pager.next { text-align: right; align-items: flex-end; }
.pager-role {
  font-size: .8125rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--nav-ink); white-space: nowrap;
}
/* The labels are `nowrap`, so on the narrowest phones they do not wrap, they
   OVERFLOW: at 320px the three columns come to exactly the content width and
   "PREVIOUS" ran under "PICK RANDOM" with about three pixels between them.
   Set smaller and tighter there rather than letting them wrap, because "PICK /
   RANDOM" over two lines is a worse row than a slightly smaller one. */
@media (max-width: 23.5rem) {
  .pager-role { font-size: .75rem; letter-spacing: .03em; }
  .unit-nav { gap: .5rem; }
}
/* The chevrons live in CSS, not in the text, so a screen reader reads
   "Previous" and not "left angle bracket Previous". */
.pager.prev .pager-role::before { content: "\2039\00a0"; }
.pager.next .pager-role::after { content: "\00a0\203a"; }
/* HYPHENATE, NEVER CHOP. The cell is 103px on a 390px phone and
   "Weatherbeard" wants 107, so `overflow-wrap: anywhere` broke it after the r
   and the foot of The Nettle Spinner offered the reader "Farmer Weatherbear /
   d". `break-word` is no better: at 320px it made "Rapunzel" into "Rapunze /
   l". A story's title is the one string on this page a reader is meant to
   recognise, and four pixels is not worth making it unrecognisable.

   So the titles hyphenate where English allows it and otherwise overflow their
   cell by those few pixels. That is safe HERE and not in general: the titles
   sit on the grid's second row, where the middle cell is empty, so a title
   wider than its column runs into nothing. */
.pager-title {
  font-size: .9375rem; color: var(--muted); line-height: 1.3;
  hyphens: auto;
}
.pager:hover { background: var(--card); }
.pager:hover .pager-title { color: var(--fg); }
/* No box on the shuffle. It was drawn as a bordered control on the reasoning
   that it is an action rather than a place, which made one of three sibling
   links look like a different species and pushed its label off the line the
   other two share. Three cells, one kind, one row. */

/* --- indexes */
.a-to-z, .tag-list, .tag-cards { list-style: none; padding: 0; }
.a-to-z li { margin: .3rem 0; }
.a-to-z .letter { font-family: var(--sans); font-size: .8125rem; letter-spacing: .12em;
  color: var(--muted); margin-top: 1.5rem; }
.tag-list { display: flex; flex-wrap: wrap; gap: .5rem; }
.tag-list a { display: inline-flex; gap: .4rem; align-items: baseline;
  border: 1px solid var(--edge); border-radius: 999px; padding: .25rem .75rem;
  text-decoration: none; font-family: var(--sans); font-size: .875rem; }
.tag-list .count { color: var(--muted); font-size: .75rem; }
.tag-cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); }
.tag-card { border-left: 4px solid var(--accent); padding-left: .9rem; }
.tag-card a { text-decoration: none; color: inherit; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* --- touch targets
 *
 * A tale's title in the contents list renders as a 19px inline link, and a book
 * hub is 37 of them stacked. On a phone that is a column of neighbours a thumb
 * cannot separate, and mis-tapping into the wrong story is the one navigation
 * failure a reader cannot work around: they do not know which story they meant
 * to open until they have lost it. Measured at 390px before this existed: 35
 * targets under 24px on the hub, 36 on the A-to-Z.
 *
 * Widened only where the pointer is COARSE. A mouse is precise at 19px and a
 * dense list is easier to scan, so paying for touch on every device would make
 * the desktop worse to fix something desktop does not have.
 */
@media (pointer: coarse) {
  /* .toc is deliberately absent. Its rows are already a full-width grid with
   * .7rem of padding, and the `display: block` this block used to apply to them
   * flattened that grid back into a stack, collapsing the number gutter on
   * exactly the devices the rule exists to serve. A rule that only fires on
   * touch is a rule nobody sees break. */
  .a-to-z li a,
  .matter a { display: block; padding: .5rem 0; }
  .a-to-z li { margin: 0; }
}
