@charset "utf-8";
/* ==========================================================================
   bookatello.com - the one stylesheet
   --------------------------------------------------------------------------
   Plain CSS. No preprocessor, no build step, no CDN, no @import of anything
   remote. The Content-Security-Policy in _headers blocks external assets at
   runtime, so this cannot quietly regress.

   Motif: a Florentine bookbinder's bench. Warm paper, ink, a terracotta bench
   edge, one hairline of gold leaf. Everything reads as a sheet lying on a
   bench - which is literally what the product is.

   Two rules that govern every decision below, because the audience skews
   seniors and low-vision readers:
     1. Nothing ships below 4.5:1 contrast. Every pair here was measured with
        the WCAG formula, in BOTH colour schemes, not eyeballed.
     2. Nothing ships below 17px. There is no fine print on this site,
        including the privacy page.

   CONTENTS
     1. Tokens                    6. Cards + download components
     2. Reset + base              7. Opt-in form
     3. Typography                8. Footer
     4. Layout + containers       9. Utilities
     5. Header + nav             10. Print
   ========================================================================== */

/* ==========================================================================
   1. TOKENS
   ========================================================================== */

:root {
  /* --- light (default) -------------------------------------------------- */
  --paper:           #FAF6EF;  /* page background                           */
  --sheet:           #FFFDF9;  /* cards                                     */
  --ink:             #2A2118;  /* body text      - 14.67:1 on paper         */
  --ink-soft:        #5A4C3C;  /* captions, meta -  7.69:1 on paper         */
  --terracotta:      #9C3D1F;  /* links, H1/H2   -  6.29:1 on paper         */
  --terracotta-deep: #8A3218;  /* button fill    -  8.24:1 vs white text    */
  --verdigris:       #3E5C4B;  /* proof marks    -  6.87:1 on paper         */
  --gold:            #B07B2E;  /* ORNAMENT ONLY  -  3.41:1, never text      */
  --rule:            #E3D9C8;  /* decorative hairlines (1.30:1)             */
  --rule-strong:     #8A7A63;  /* input + control borders - 3.86:1, WCAG 1.4.11 */
  --focus:           #2A2118;
  --play-peer:       #F3EBDC;  /* peer row/col/box tint                     */
  --play-same:       #EADFC8;  /* same digit elsewhere on the board          */
  --play-sel:        #E8D9BF;  /* the selected cell                          */
  --play-bad:        #9B1B0E;  /* a digit that contradicts another - 7.53:1  */

  /* Text that sits ON a filled control. Per-scheme, because a fill that is
     dark in light mode becomes light in dark mode and white text on it would
     drop to 2.63:1 - the bug this token exists to prevent. */
  --btn-ink:         #FFFFFF;  /* 8.24:1 on --terracotta-deep               */

  /* The footer band gets its OWN tokens rather than reusing --ink/--paper.
     Reusing them made the band invert in dark mode into a bright slab at the
     bottom of a dark page, and dragged the link-hover colour to 1.66:1. */
  --footer-bg:       #2A2118;
  --footer-ink:      #FAF6EF;  /* 14.67:1 */
  --footer-hover:    #F0C08A;  /*  9.49:1 - a tint, never --gold (3.41:1) */

  /* Text on a type chip. FIXED - deliberately does NOT flip with the theme.
     Chip backgrounds are the eight dark cover palettes in both schemes, so
     the label must stay light in both. Measured range 7.92:1 - 14.27:1. */
  --chip-ink:        #FAF6EF;

  /* --- the eight per-type accents ---------------------------------------
     Lifted verbatim from src/kdp/cover_generator.py so the site and the
     printed books agree at a glance. Used ONLY as a 6px top rule on a type
     page and as the type chip. Never body text, never a page background. */
  --type-sudoku:       #2A241A;
  --type-maze:         #1E3D6B;
  --type-crossword:    #1B2A44;
  --type-wordsearch:   #11474A;
  --type-fillin:       #3B1B25;
  --type-codeword:     #3A1A5E;
  --type-killersudoku: #124B22;
  --type-calcudoku:    #7A3B12;

  /* --- type ------------------------------------------------------------- */
  --font-display: "Noto Serif", Georgia, "Times New Roman", serif;
  --font-body: "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;

  /* --- spacing (4px base) ----------------------------------------------- */
  --s1: 0.25rem; --s2: 0.5rem;  --s3: 0.75rem; --s4: 1rem;
  --s6: 1.5rem;  --s8: 2rem;    --s12: 3rem;   --s16: 4rem;  --s24: 6rem;

  --container: 68rem;
  --measure: 36rem;      /* ~64ch of body text */
  --radius: 4px;         /* maximum. A pill reads as a phone app;
                            a slight radius reads as a cut sheet. */
}

@media (prefers-color-scheme: dark) {
  :root {
    --paper:           #1C1814;
    --sheet:           #241F1A;
    --ink:             #EDE6DA;  /* 14.23:1 */
    --ink-soft:        #C4B6A2;  /*  8.87:1 */
    --terracotta:      #E08A63;  /*  6.71:1 */
    --terracotta-deep: #E08A63;
    --verdigris:       #86B69C;  /*  7.72:1 */
    --gold:            #D9AE63;  /* still ornament only, for consistency    */
    --rule:            #3A322A;
    --rule-strong:     #857868;  /*  4.10:1 on paper, 3.80:1 on sheet       */
    --focus:           #EDE6DA;
    --play-peer:       #2A241E;
    --play-same:       #332C24;
    --play-sel:        #392F26;
    --play-bad:        #FF9078;
    --btn-ink:         #1C1814;  /*  6.71:1 on the light terracotta fill    */
    --footer-bg:       #141110;
    --footer-ink:      #EDE6DA;  /* 15.15:1 */
    --footer-hover:    #E08A63;  /*  7.15:1 */

  /* The eight per-type chip colours were NOT overridden for dark mode, so they
     kept their light-mode values -- which are dark by design, because they sit
     on cream paper. On the dark card (--sheet #241F1A) every one of them fell
     below the 3:1 WCAG floor for a non-text graphic, and --type-sudoku
     (#2A241A) landed at 1.06:1: the chip had no visible fill at all and read
     as bare text while its neighbours were solid blocks.

     Same hue and saturation, raised in lightness only, so each chip still
     matches its book cover. Each clears 3:1 against the card, and the
     --chip-ink label clears 4.5:1 on the new fill. */
  --type-sudoku:       #79684B;  /* 3.01:1 on card, label 5.04:1 */
  --type-maze:         #346ABB;  /* 3.06:1               4.95:1 */
  --type-crossword:    #456BAE;  /* 3.10:1               4.89:1 */
  --type-wordsearch:   #1C777C;  /* 3.09:1               4.91:1 */
  --type-fillin:       #A74D69;  /* 3.04:1               4.99:1 */
  --type-codeword:     #8749CD;  /* 3.01:1               5.03:1 */
  --type-killersudoku: #1E7C38;  /* 3.12:1               4.86:1 */
  --type-calcudoku:    #AB5319;  /* 3.09:1               4.91:1 */
  }
}

/* ==========================================================================
   2. RESET + BASE
   ========================================================================== */

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

html {
  font-size: 100%;            /* NEVER a px value - it would override the
                                 reader's own browser font-size preference,
                                 which is exactly the setting this audience
                                 has already gone and changed. */
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.1875rem;       /* 19px - one notch above the 18px floor, so a
                                 reader's zoom starts from headroom */
  line-height: 1.65;
  text-rendering: optimizeLegibility;
  /* Long unbreakable strings -- "hello@bookatello.com" is the one that bit --
     must wrap rather than push the page sideways. At 200% zoom on a 375px
     screen that address alone measured 397px. This only breaks a word that
     would otherwise overflow, so ordinary prose is untouched: no hyphenation,
     no justification, nothing that damages word shape for a low-vision
     reader.

     It must be `anywhere` and not `break-word`: only `anywhere` is allowed to
     affect an element's intrinsic min-content width, and every stubborn case
     here was a shrink-to-fit box (an inline-block link, a chip) that had
     already sized itself to max-content before any wrapping was considered.
     With `break-word` the email address still measured 397px in a 279px
     column. */
  overflow-wrap: anywhere;
}

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

/* --- fonts, self-hosted --------------------------------------------------
   Subset with tools/site_fonts.py from this repo's already-licensed
   assets/fonts/. NEVER hotlink Google Fonts: that leaks every visitor's IP
   to a third party and is the exact GDPR trap the no-CDN rule exists for.
   font-display: swap - a font that never arrives must not mean text that
   never arrives, least of all for a reader on rural DSL.                  */
@font-face {
  font-family: "Noto Sans";
  src: url("../fonts/NotoSans-Regular.ttf") format("truetype");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Noto Sans";
  src: url("../fonts/NotoSans-Bold.ttf") format("truetype");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Noto Serif";
  src: url("../fonts/NotoSerif-Regular.ttf") format("truetype");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Noto Serif";
  src: url("../fonts/NotoSerif-Bold.ttf") format("truetype");
  font-weight: 700; font-style: normal; font-display: swap;
}

/* --- focus, declared BEFORE :hover so it can never be overridden by it --- */
:focus { outline: 3px solid var(--focus); outline-offset: 3px; }
:focus:not(:focus-visible) { outline: none; }
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }

/* --- motion: a 120ms colour transition, and nothing else ----------------- */
a, .btn, .download-card, summary {
  transition: color 120ms ease, background-color 120ms ease, border-color 120ms ease;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   3. TYPOGRAPHY
   --------------------------------------------------------------------------
   Fluid headings use clamp() with a rem term in the fluid middle, never pure
   vw. A vw-only clamp does not respond to text-only zoom, which is the
   classic 200% failure and would land squarely on this audience.
   ========================================================================== */

h1, h2, h3, h4 {
  margin: 0 0 var(--s4);
  color: var(--ink);
  text-wrap: balance;
  /* 30ch, not 22ch. At 22ch both "Free puzzles. Print them now." and "Eight
     sets, free, right now" wrapped to two lines, and those two extra lines
     were enough to push the first download card below the fold on a 1366x768
     laptop. 30ch still keeps a heading well inside comfortable reading width. */
  max-width: 30ch;
}

h1 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(2rem, 1.7rem + 1.2vw, 2.5rem);   /* 32 -> 40px */
  line-height: 1.15;
  letter-spacing: -0.01em;
}

h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.6875rem, 1.5rem + 0.8vw, 2rem); /* 27 -> 32px */
  line-height: 1.2;
  margin-top: var(--s8);
}

h3 {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: clamp(1.4375rem, 1.35rem + 0.4vw, 1.625rem); /* 23 -> 26px */
  line-height: 1.3;
}

h4 {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 1.1875rem;
  line-height: 1.4;
}

p, li, dd, dt, label, figcaption { max-width: var(--measure); }
p { margin: 0 0 var(--s4); }

a { color: var(--terracotta); text-decoration-thickness: 2px; text-underline-offset: 3px; }
a:hover { color: var(--ink); }

strong, b { font-weight: 700; }

/* the lead paragraph under an h1 */
.lead {
  font-size: clamp(1.25rem, 1.2rem + 0.2vw, 1.375rem);  /* 20 -> 22px */
  line-height: 1.5;
  color: var(--ink);
  max-width: var(--measure);
}

/* the floor. Nothing on this site is smaller than this, anywhere. */
.small, .meta, .filesize { font-size: 1.0625rem; line-height: 1.5; color: var(--ink-soft); }

/* Uppercase label above an h2. Kept to three words maximum - all-caps
   destroys word shape, the exact cue a low-vision reader depends on. */
.eyebrow {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  line-height: 1.4;
  margin: 0 0 var(--s2);
  display: flex;
  align-items: center;
  gap: var(--s2);
}

ul, ol { padding-left: 1.4em; margin: 0 0 var(--s4); }
li { margin-bottom: var(--s2); }

/* the wedge as a list bullet */
.wedge-list { list-style: none; padding-left: 0; }
.wedge-list li {
  position: relative;
  padding-left: 1.6em;
  max-width: var(--measure);
}
.wedge-list li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.55em;
  width: 0; height: 0;
  border-left: 9px solid var(--terracotta);
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
}

/* ==========================================================================
   4. LAYOUT
   --------------------------------------------------------------------------
   Single-column FIRST. Grids ADD columns above a breakpoint, never the
   reverse, so 200% zoom reflows instead of scrolling sideways. No fixed
   heights anywhere. DOM order IS reading order: every page must read
   correctly with CSS entirely disabled.
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--s6);
}
@media (min-width: 45rem) { .container { padding-inline: var(--s8); } }

.section { padding-block: var(--s16); }
@media (min-width: 45rem) { .section { padding-block: var(--s24); } }

/* The hero and the band directly beneath it are deliberately tighter than the
   standard rhythm. The free puzzles are a HARD above-the-fold requirement:
   with the default 96px block padding the first download button landed at
   1822px on an 800px-tall viewport, which is a broken promise, not a
   styling preference. Measured, not guessed. */
.section--hero  { padding-block: var(--s8) var(--s6); }
.section--tight { padding-block: var(--s8); }
@media (min-width: 45rem) {
  .section--hero  { padding-block: var(--s12) var(--s8); }
  .section--tight { padding-block: var(--s12); }
}

.prose { max-width: var(--measure); }

/* Grid tracks must be allowed to shrink BELOW their content's min-content
   width, or the layout blows out sideways at high zoom.

   A grid item defaults to `min-width: auto`, i.e. "never narrower than my
   min-content". At 200% text-only zoom on a 375px phone that floor grew to
   375px inside a 279px grid, so the track pushed the whole document wide and
   the reader got a horizontal scrollbar -- WCAG 1.4.10, and the single worst
   outcome for the low-vision audience this site is built for.

   `minmax(0, 1fr)` removes that floor. `min(20rem, 100%)` does the same job for
   the multi-column rule: the track wants 20rem but never more than the
   container, so it collapses to one column instead of overflowing. */
.grid {
  display: grid;
  gap: var(--s6);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 45rem) {
  .grid { grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); }
}

/* --- the bench edge: the signature divider -------------------------------
   A 3px terracotta rule with a 1px gold hairline 2px beneath it. Two CSS
   borders, zero images. It appears on every page and is the single element
   that makes the site feel like one object. */
.bench-rule {
  border: 0;
  height: 6px;
  margin: var(--s12) 0;
  background:
    linear-gradient(var(--terracotta), var(--terracotta)) top    / 100% 3px no-repeat,
    linear-gradient(var(--gold),       var(--gold))       bottom / 100% 1px no-repeat;
}

/* --- graph-paper ground: puzzle-native texture, one declaration, 0 bytes.
   3% ink at 40px pitch - too faint to touch the contrast of anything on it. */
.graph-ground {
  background-image:
    repeating-linear-gradient(to right,  color-mix(in srgb, var(--ink) 3%, transparent) 0 1px, transparent 1px 40px),
    repeating-linear-gradient(to bottom, color-mix(in srgb, var(--ink) 3%, transparent) 0 1px, transparent 1px 40px);
}

.skip-link {
  position: absolute;
  max-width: calc(100% - var(--s6));   /* stays on screen at high zoom */
  left: var(--s4); top: -100vh;
  z-index: 100;
  background: var(--terracotta-deep);
  color: var(--btn-ink);
  padding: var(--s3) var(--s6);
  border-radius: var(--radius);
  font-weight: 700;
  text-decoration: none;
}
.skip-link:focus { top: var(--s4); }

/* ==========================================================================
   5. HEADER + NAV
   ========================================================================== */

.site-header {
  border-bottom: 1px solid var(--rule);
  background: var(--sheet);
}
.site-header > .container {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  padding-block: var(--s4);
}

.wordmark { text-decoration: none; color: var(--ink); display: inline-flex; align-items: center; gap: var(--s3); }
.wordmark:hover { color: var(--ink); }
.wordmark .mark { width: 24px; height: 32px; color: var(--terracotta); flex: none; }
.wordmark-text { display: block; }
/* The wordmark is the ONE place a viewport cap is allowed on this site.
   "BOOKATELLO" is a single unbreakable word: at 200% text-only zoom on a 375px
   phone it wanted 378px on its own and pushed the whole document 99px wide,
   which is a WCAG 1.4.10 reflow failure -- and horizontal scrolling is exactly
   what a low-vision reader on a phone cannot cope with.

   min() caps growth ONLY where the screen is genuinely too narrow: the vw term
   binds below roughly 360-420px and is otherwise ignored, so on any normal
   screen the wordmark still scales fully with the reader's own font-size
   setting. Body copy, nav and headings are untouched and still scale without
   limit -- this cap applies to a decorative lockup whose text is repeated in
   the page <title> and the nav. */
.wordmark-name {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: min(1.625rem, 7.2vw);
  letter-spacing: 0.02em;
  line-height: 1.1;
}
.wordmark-sub {
  display: block;
  font-size: min(1.0625rem, 4vw);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  border-top: 2px solid var(--gold);
  margin-top: 2px;
  padding-top: 2px;
}

.site-nav ul {
  list-style: none;
  margin: 0; padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s6);
}
.site-nav li { margin: 0; }
.site-nav a {
  display: block;
  padding: var(--s4) 0;      /* 16px vertical, not 4 - this is a tap target */
  font-weight: 700;
  text-decoration: none;
  color: var(--ink);
  border-bottom: 3px solid transparent;
}
/* On a phone the nav wraps to two rows, and at 16px of vertical padding per
   row the header was taking 277px of an 812px screen -- a third of the
   viewport before a single word of content. Trimmed to 12px, which still
   leaves each link a ~52px tap target, comfortably over the 44px minimum. */
@media (max-width: 45rem) {
  .site-header > .container { padding-block: var(--s3); gap: var(--s2); }
  .site-nav ul { gap: 0 var(--s4); }
  .site-nav a { padding: var(--s3) 0; }
}

.site-nav a:hover, .site-nav a[aria-current="page"] {
  color: var(--terracotta);
  border-bottom-color: var(--terracotta);
}

/* ==========================================================================
   6. CARDS + DOWNLOAD COMPONENTS
   --------------------------------------------------------------------------
   Cards are SHEETS: 1px rule border, a 2px gold rule inset along the top, and
   a HARD OFFSET shadow. Zero blurred shadows anywhere on this site - blur
   reads as SaaS dashboard, hard offset reads as paper on wood.
   ========================================================================== */

.card, .download-card, .book-card, .callout {
  background: var(--sheet);
  border: 1px solid var(--rule);
  border-top: 2px solid var(--gold);
  border-radius: 2px;
  box-shadow: 2px 2px 0 var(--rule);
  padding: var(--s6);
}

/* THE core component. The WHOLE card is the link - a 320x180px target
   instead of a 200x19px one. */
.download-card {
  display: block;
  text-decoration: none;
  color: var(--ink);
  height: 100%;
}
.download-card:hover { border-color: var(--rule-strong); color: var(--ink); }
.download-card h3 { margin-bottom: var(--s2); color: var(--terracotta); }
.download-card:hover h3 { color: var(--ink); }

/* A card's title link is a primary navigation target, not an inline link in a
   sentence, so WCAG 2.5.8's inline exemption does not cover it. Measured at
   32px unstyled; the padding brings it to 48. */
.card h3 a, .book-card h3 a {
  display: inline-block;
  padding: var(--s2) 0;
}

/* A paragraph whose entire content is one link is a navigation target too --
   the "-> See the large-print sets" pattern. Same reasoning, same fix. */
.prose > p > a:only-child,
.container > p > a:only-child {
  display: inline-block;
  padding: var(--s2) 0;
}
.download-card p { margin-bottom: var(--s3); }
.download-card .preview {
  border: 1px solid var(--rule);
  margin-bottom: var(--s4);
  background: #FFFFFF;
}

.type-chip {
  display: inline-block;
  max-width: 100%;                 /* never wider than its column at high zoom */
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: var(--s1) var(--s3);
  border-radius: 2px;
  margin-bottom: var(--s3);
  color: var(--chip-ink);          /* fixed light ink - see token note */
  background: var(--ink);
}
.type-chip--sudoku       { background: var(--type-sudoku); }
.type-chip--maze         { background: var(--type-maze); }
.type-chip--crossword    { background: var(--type-crossword); }
.type-chip--wordsearch   { background: var(--type-wordsearch); }
.type-chip--fillin       { background: var(--type-fillin); }
.type-chip--codeword     { background: var(--type-codeword); }
.type-chip--killersudoku { background: var(--type-killersudoku); }
.type-chip--calcudoku    { background: var(--type-calcudoku); }

/* the 6px top rule on a type page */
.type-rule { height: 6px; border: 0; margin: 0; background: var(--ink); }
.type-rule--sudoku       { background: var(--type-sudoku); }
.type-rule--maze         { background: var(--type-maze); }
.type-rule--crossword    { background: var(--type-crossword); }
.type-rule--wordsearch   { background: var(--type-wordsearch); }
.type-rule--fillin       { background: var(--type-fillin); }
.type-rule--codeword     { background: var(--type-codeword); }
.type-rule--killersudoku { background: var(--type-killersudoku); }
.type-rule--calcudoku    { background: var(--type-calcudoku); }

/* THE PROOF MARK. Plain text, so it survives images-off and screen readers
   with no extra work. It appears on sudoku, killer sudoku, calcudoku and
   word fill-ins and NOWHERE ELSE - its selectivity is what makes it mean
   anything. Colour is never the sole signal: it also carries a check
   character and the word "Checked". */
.proof {
  color: var(--verdigris);
  font-weight: 700;
  font-size: 1.0625rem;
  margin: var(--s3) 0;
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}

.spec { margin: 0 0 var(--s4); }
.spec div { display: flex; flex-wrap: wrap; gap: var(--s2); border-bottom: 1px solid var(--rule); padding: var(--s2) 0; }
.spec dt { font-weight: 700; margin: 0; min-width: 11rem; }
.spec dd { margin: 0; color: var(--ink-soft); }

.filesize { display: block; margin-top: var(--s3); }

/* --- buttons -------------------------------------------------------------
   56px minimum height, 4px radius maximum, full width under 480px,
   >=12px between adjacent targets. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: 56px;
  min-width: 48px;
  max-width: 100%;                 /* never wider than its column at high zoom */
  /* the vw term only binds on a very narrow column (extreme zoom), where the
     label matters far more than the generous side padding */
  padding: var(--s4) min(1.75rem, 6vw);
  border: 2px solid transparent;
  border-radius: var(--radius);
  font-family: var(--font-body);
  font-size: 1.1875rem;
  font-weight: 700;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.btn--primary { background: var(--terracotta-deep); color: var(--btn-ink); border-color: var(--terracotta-deep); }
.btn--primary:hover { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.btn--secondary { background: transparent; color: var(--verdigris); border-color: var(--verdigris); }
.btn--secondary:hover { background: var(--verdigris); color: var(--paper); }

.dl-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);              /* 12px minimum between adjacent targets */
  margin: var(--s4) 0;
}
@media (max-width: 30rem) {
  .dl-row .btn { width: 100%; }
}

.callout { border-top: 2px solid var(--rule-strong); border-color: var(--rule-strong); }
.callout h2, .callout h3 { margin-top: 0; }

/* the /bonus/ find-your-book list: plain <ul> of <a>, no search box, no JS */
.code-list { list-style: none; padding: 0; margin: var(--s6) 0; max-width: var(--measure); }
.code-list li { margin: 0; border-bottom: 1px solid var(--rule); }
.code-list a {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s4);
  min-height: 48px;
  padding: var(--s4) var(--s2);
  font-size: 1.5rem;           /* 24px */
  text-decoration: none;
}
.code-list a:hover { background: var(--sheet); }
.code-list .code { font-weight: 700; font-family: var(--font-body); min-width: 4.5rem; }
.code-list .title { color: var(--ink); font-size: 1.1875rem; }

.book-card h3 { margin-top: 0; }

/* ==========================================================================
   7. OPT-IN
   ========================================================================== */

.optin {
  background: var(--sheet);
  border: 1px solid var(--rule);
  border-top: 2px solid var(--gold);
  box-shadow: 2px 2px 0 var(--rule);
  border-radius: 2px;
  padding: var(--s6);
  max-width: var(--measure);
}
.optin label { display: block; font-weight: 700; margin-bottom: var(--s2); }
.optin input[type="email"], .optin input[type="text"] {
  display: block;
  width: 100%;
  min-height: 56px;
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
  font-family: var(--font-body);
  font-size: 1.1875rem;
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--rule-strong);   /* 3.86:1 - WCAG 1.4.11 */
  border-radius: var(--radius);
}
.optin fieldset { border: 1px solid var(--rule); border-radius: 2px; padding: var(--s4); margin: 0 0 var(--s4); }
.optin legend { font-weight: 700; padding-inline: var(--s2); }
.optin .radio { display: flex; align-items: center; gap: var(--s3); min-height: 48px; }
.optin .radio input { width: 24px; height: 24px; }
.optin .radio label { margin: 0; font-weight: 400; }
.optin .btn { width: 100%; }

/* honeypot: visually gone, but NOT display:none on the input itself -
   aria-hidden on the wrapper is what keeps screen readers away from it. */
.hp {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ==========================================================================
   8. FOOTER  (shared byte-for-byte across all 22 pages)
   ========================================================================== */

.site-footer {
  background: var(--footer-bg);
  color: var(--footer-ink);
  padding-block: var(--s16);
  margin-top: var(--s16);
}
.site-footer a { color: var(--footer-ink); }
/* Hover changes colour AND thickens the underline: colour is never the sole
   signal. The hover tint is NOT --gold - gold is 4.30:1 on this band, which
   fails, and 1.66:1 in dark mode, which fails badly. */
.site-footer a:hover { color: var(--footer-hover); text-decoration-thickness: 3px; }
.site-footer h2, .site-footer h3 { color: var(--footer-ink); }
.site-footer :focus-visible { outline-color: var(--footer-ink); }
.footer-brand {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.625rem;
  margin-bottom: var(--s6);
}
.footer-cols {
  display: grid;
  gap: var(--s6);
  grid-template-columns: 1fr;
  margin-bottom: var(--s8);
}
@media (min-width: 45rem) { .footer-cols { grid-template-columns: repeat(3, 1fr); } }
.footer-cols h3 { font-size: 1.1875rem; margin-bottom: var(--s2); }
.footer-cols ul { list-style: none; padding: 0; margin: 0; }
.footer-cols li { margin: 0; }
.footer-cols a { display: block; padding: var(--s2) 0; min-height: 48px; }
.footer-legal { max-width: var(--measure); }
.footer-legal p { color: var(--footer-ink); }

/* ==========================================================================
   9. UTILITIES
   ========================================================================== */

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.mark { fill: currentColor; }
.stack > * + * { margin-top: var(--s4); }
.center-narrow { max-width: var(--measure); }

/* ==========================================================================
   11. PLAY  (site v2 - the in-browser puzzle board)
   --------------------------------------------------------------------------
   Sized for a thumb and an older eye, in that order. The board is capped at
   34rem so it never outgrows a phone, the number pad lives at the BOTTOM of
   the screen where a thumb actually reaches, and every key clears 56px.

   There are no pencil marks. Three legible digits do not fit inside a 40px
   phone cell without going under this site's 17px floor, and going under the
   floor for THIS audience is not a trade worth making.
   ========================================================================== */

/* The play page trims its own hero: on a game page the board is the content,
   and every pixel above it is a pixel of scrolling between a visitor and the
   thing they came for. */
.section--play { padding-block: var(--s4) var(--s6); }
.section--play h1 { margin-bottom: var(--s2); }
.section--play .lead { margin-bottom: var(--s4); }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-bottom: var(--s4);
}
.hero-actions .btn { min-height: 52px; }

.play {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s4);
}

/* On a wide screen the board shares the heading's left edge instead of
   floating centre-stage under left-aligned copy, which read as two unrelated
   blocks stacked by accident. */
@media (min-width: 45rem) {
  .play { align-items: flex-start; }
}

.play-levels {
  display: flex;
  gap: var(--s2);
  width: min(96vw, 34rem);
}

.level {
  flex: 1 1 0;
  padding: var(--s3) var(--s2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--sheet);
  color: var(--ink);
  font-weight: 700;
  text-align: center;
  text-decoration: none;
  min-height: 48px;
}

.level.is-on {
  background: var(--terracotta-deep);
  border-color: var(--terracotta-deep);
  color: var(--btn-ink);
}

.play-bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s2) var(--s4);
  width: min(96vw, 34rem);
}

.play-clock {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  min-width: 4ch;
}

.play-meta { color: var(--ink-soft); flex: 1 1 12rem; margin: 0; }

.play-status {
  width: 100%;
  max-width: 34rem;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  color: var(--ink);
}

.play-status.is-error { border-color: var(--play-bad); color: var(--play-bad); }

/* --- the board ---------------------------------------------------------- */

.play-grid {
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  width: min(96vw, 34rem);
  aspect-ratio: 1 / 1;
  border: 3px solid var(--ink);
  background: var(--ink);
  gap: 1px;
}

.sq {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  padding: 0;
  background: var(--sheet);
  color: var(--terracotta);        /* a digit YOU entered */
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 5.2vw, 1.9rem);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* The 3x3 boxes. Drawn as thicker cell edges rather than as an overlay, so
   they survive any grid size and never drift out of alignment. */
.sq--boxr { border-right: 2px solid var(--ink); }
.sq--boxb { border-bottom: 2px solid var(--ink); }

.sq.is-given { color: var(--ink); font-weight: 700; }
.sq.is-peer  { background: var(--play-peer); }
.sq.is-same  { background: var(--play-same); }
.sq.is-sel   { background: var(--play-sel); box-shadow: inset 0 0 0 3px var(--terracotta-deep); }
/* A conflict is signalled THREE ways, not one. --play-bad and --terracotta
   are neighbouring warm hues by design (the palette is warm throughout), so
   colour alone would ask a reader to tell salmon from terracotta in a 38px
   cell -- exactly the discrimination that declines with age, and a WCAG 1.4.1
   failure besides. Weight and the underline carry the message on their own. */
.sq.is-bad   {
  color: var(--play-bad);
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 3px;
  text-underline-offset: 3px;
}
.sq:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }

/* --- the number pad ----------------------------------------------------- */

.play-pad {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--s2);
  width: min(96vw, 34rem);
}

/* On a phone the board alone fills the screen, so a pad that scrolled with
   the page would mean scrolling down to type and back up to see the result,
   for every digit. Sticking it to the bottom edge is what every sudoku app
   does, and for the same reason. It sticks only while the board is on screen,
   so it never floats over the prose further down. */
@media (max-width: 45rem) {
  .play-pad {
    position: sticky;
    bottom: 0;
    z-index: 2;
    padding: var(--s2) 0 max(var(--s2), env(safe-area-inset-bottom));
    background: var(--paper);
    border-top: 2px solid var(--rule);
  }
}

.pad-key {
  min-height: 56px;
  padding: var(--s2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--sheet);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 1.5rem;
  cursor: pointer;
  touch-action: manipulation;
}

.pad-key:hover { background: var(--play-peer); }
.pad-key:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

/* A digit already placed nine times. Dimmed, never disabled: disabling it
   would move focus unpredictably mid-game. */
.pad-key.is-spent { color: var(--ink-soft); border-style: dashed; }

/* Erase occupies the tenth slot of a 5x2 pad, so the nine digits and the
   eraser form two clean rows. Spanning two columns pushed it onto a third row
   of its own, which wasted a band of the screen the board wanted. */
.pad-key--erase {
  font-family: var(--font-body);
  font-size: 1.125rem;
}

.play-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  justify-content: center;
  width: min(96vw, 34rem);
}

.play-controls .btn { flex: 1 1 auto; min-height: 48px; }

/* --- word search --------------------------------------------------------
   12 columns, not the printable's 15: at 15 a 375px phone gives 24px cells,
   and this site's floor is 17px type. A letter that size in a cell that size
   is cramped for exactly the readers the floor exists for. */

.ws-themes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  width: min(96vw, 34rem);
}

.ws-theme {
  padding: var(--s2) var(--s3);
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--sheet);
  color: var(--ink);
  font-weight: 700;
  text-decoration: none;
}

.ws-theme.is-on {
  background: var(--terracotta-deep);
  border-color: var(--terracotta-deep);
  color: var(--btn-ink);
}

.ws-grid {
  display: grid;
  grid-template-columns: repeat(var(--ws-cols, 12), 1fr);
  width: min(96vw, 34rem);
  aspect-ratio: 1 / 1;
  gap: 0;
  border: 3px solid var(--ink);
  background: var(--sheet);
  /* the finger is drawing a line, not scrolling the page */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.ws-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  margin: 0;
  padding: 0;
  background: transparent;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: clamp(1.0625rem, 3.6vw, 1.375rem);
  line-height: 1;
  cursor: pointer;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
}

.ws-cell.is-sel   { background: var(--play-sel); }
.ws-cell.is-found { background: var(--play-same); color: var(--verdigris); font-weight: 700; }
.ws-cell:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }

.ws-words {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--s2) var(--s4);
  width: min(96vw, 34rem);
}

.ws-word { color: var(--ink); }

.ws-word.is-found {
  color: var(--ink-soft);
  text-decoration: line-through;
  text-decoration-thickness: 2px;
}

/* --- the finish --------------------------------------------------------- */

.play-win {
  width: min(96vw, 34rem);
  padding: var(--s6);
  border: 2px solid var(--verdigris);
  border-radius: var(--radius);
  background: var(--sheet);
  text-align: center;
}

.play-win h2 { margin-top: 0; color: var(--verdigris); }
.play-win .play-clock { font-size: 2rem; }

@media (min-width: 40rem) {
  .play-pad { grid-template-columns: repeat(10, 1fr); }
}

/* ==========================================================================
   10. PRINT
   --------------------------------------------------------------------------
   Required, not an afterthought. A library site whose pages print badly is
   telling on itself - and a senior may well print the page rather than the PDF.
   ========================================================================== */

@media print {
  .site-nav, .site-footer, .optin, .skip-link, .bench-rule { display: none; }
  .play, .play-pad, .play-controls, .ws-themes { display: none; }
  body { color: #000; background: #fff; font-size: 12pt; }
  .container { max-width: none; padding: 0; }
  a { color: #000; }
  a[href^="http"]::after, a[href^="/"]::after {
    content: " (" attr(href) ")";
    font-size: 10pt;
  }
  .download-card, .card, .book-card, .callout {
    break-inside: avoid;
    border: 1pt solid #000;
    box-shadow: none;
  }
  h1, h2, h3 { break-after: avoid; }
}
