/* PageLab — public site.
   Palette lifted from the Android app (res/values/colors.xml) so the web front door and the
   product read as one thing: midnight-forest ground, warm parchment paper, amber lamp glow.
   The app is a dark, lamp-lit reading room; the hero honours that, then the page opens onto
   parchment the way the app's sheets do. */

:root {
  --midnight-950: #070C0B;
  --midnight-900: #0A100E;
  --midnight-800: #111816;
  --midnight-700: #1A231F;
  --midnight-600: #25302B;

  --forest-900: #142A1B;
  --forest-800: #1F3F28;
  --forest-700: #2A5536;
  --forest-500: #3F7D4F;
  --forest-200: #A0C0A7;
  --forest-100: #C6DACB;

  --amber-300: #E8B964;   /* lamp_glow */
  --amber-400: #D9A648;
  --amber-500: #C2913A;
  --amber-700: #71561E;

  --terracotta: #C8553D;

  --parchment: #F4EFE4;
  --parchment-dim: #EAE3D3;
  --ink: #1B211A;
  --ink-soft: #454C42;
  --ink-muted: #6E7566;

  /* Literata leads the stack HERE, not in client.css. The @font-face lives in this file, which
     every page loads, but the token that references it used to live in client.css on
     `body.client` - a selector only app.html carries. The result was that the brand serif
     shipped, loaded, and was then used by the SPA alone: the homepage, login, signup, forgot,
     account, appeal and delete-account all fell back to Palatino, and never even downloaded
     the file. The first page a new reader ever sees was the one page the brand font missed. */
  --serif: 'Literata', 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
  --sans: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;

  --measure: 68ch;
  /* One knob for the decorative shelf. Each .spine carries a unitless --h (its desktop height in
     px) and multiplies by this LENGTH, so calc() yields px. Heights used to be hard-coded inline,
     which meant they could not shrink with the spine widths on a phone. */
  --shelf-scale: clamp(0.62px, 0.05vw + 0.36px, 1px);
  --pad: clamp(20px, 5vw, 40px);
}

/* ── Semantic theme layer ────────────────────────────────────────────────────
   The palette above is BRAND-named (--midnight-900, --parchment). Brand names can't
   flip: a token literally called "midnight" holding a cream colour is a trap for the
   next person. So the app surfaces (client.css, app.css) consume the SEMANTIC names
   below, and only these get re-pointed per theme.

   Two roles needed splitting out, because one brand token was doing both jobs:
     --accent      accent as TEXT/BORDER on the page ground — must contrast with --surface
     --accent-fill accent as a FILLED background — always carries --on-accent text
   Amber reads as text on a dark ground but not on a light one, so --accent darkens in
   light mode while --accent-fill stays amber (dark text on it works in both).

   Borders and tints are stored as bare "r,g,b" triplets so each call site keeps its own
   alpha: rgba(var(--hairline-rgb), .12). That is what makes the dark rendering below
   byte-identical to what shipped before this layer existed.

   `data-theme` is ALWAYS stamped explicitly by theme.js (it resolves `system` to a
   concrete light/dark itself), so there is no media-query-vs-attribute precedence
   fight — the attribute is the single source of truth. */
:root,
:root[data-theme="dark"] {
  /* Tells the BROWSER which scheme its own widgets should paint in. Without this the page is
     ours but the chrome is not: scrollbars, <select> popups, date pickers and autofill all
     render in the LIGHT OS scheme on top of a #0A100E ground. It sits on the same selectors as
     the palette so it can never disagree with it, and bare :root covers the pre-JS paint. */
  color-scheme: dark;
  --surface:             #0A100E;
  --surface-card:        #111816;
  --surface-inset:       #1A231F;
  --surface-active:      #1F3F28;
  --surface-accent:      #2A5536;
  --surface-veil:        rgba(10,16,14,.94);
  --surface-veil-strong: rgba(7,12,11,.72);
  --surface-spoiler:     rgba(37,48,43,.6);

  /* -- Android surface ladder, ported 2026-08-19 (values-night/themes.xml -> colors.xml).
     The web had THREE of Android's eight tonal tiers, and the three it had are already
     byte-identical to their Android counterparts:
         --surface       #0A100E = colorSurfaceDim
         --surface-card  #111816 = colorSurfaceContainerLow
         --surface-inset #1A231F = colorSurfaceContainerHigh
     So this is ADDITIVE - the five missing rungs - not a repaint. Nothing above changes value.
     NB --surface-active / --surface-accent are green ACCENT states, not mis-toned surface tiers;
     Android's neutral #1F2924 is the tier, and it lands below as --surface-highest. */
  --surface-lowest:      #070C0B;   /* colorSurfaceContainerLowest */
  --surface-base:        #0D1311;   /* colorSurface                */
  --surface-container:   #151D1A;   /* colorSurfaceContainer       */
  --surface-highest:     #1F2924;   /* colorSurfaceContainerHighest*/
  --surface-bright:      #34403A;   /* colorSurfaceBright          */

  --text:            #F4EFE4;
  --text-rgb:        244,239,228;
  --text-body:       #E4E0D2;
  --text-body-dim:   #D4D0C2;
  --text-soft:       #C6DACB;
  --text-muted:      #A0C0A7;
  /* 🐛 WAS #3F7D4F (= --forest-500) and failed AA on every dark ground it is used on: 3.89:1 on
     --surface, 3.65:1 on --surface-card, 3.26:1 on --surface-inset. It is the secondary-text
     token -- 84 call sites across client.css and app.css, all 11-14.5px body text needing 4.5:1
     -- so this one value put every timestamp, byline, thread preview, book fact and empty-state
     subtitle in the web client under AA.
     ⚠️ The binding ground is --surface-inset #1A231F, NOT --surface. Check any future value
     against inset, not against the page background. (--surface-active #1F3F28 is lighter still,
     but nothing using --text-dim ever lands on it -- every --surface-active rule sets its own
     explicit colour.) Deliberately desaturated so it reads as dim grey-green rather than as the
     forest ACCENT #6FAF7E, which secondary text must not be mistaken for.
     Now 5.67:1 / 5.32:1 / 4.75:1, and still a clear 1.71:1 step below --text-muted. */
  --text-dim:        #6C9576;

  --hairline-rgb:    160,192,167;

  --on-accent:       #0A100E;
  --danger:          #C8553D;
  --danger-rgb:      200,85,61;
  --danger-bright:   #D9694F;

  /* Form controls + status chrome (used by the account pages). */
  --field-bg:        #1A231F;
  --field-fg:        #F4EFE4;
  --alert-ok-bg:     #16241A;  --alert-ok-fg:   #A8D3B4;  --alert-ok-border:   #2A5536;
  --alert-err-bg:    #2A1714;  --alert-err-fg:  #F0A895;  --alert-err-border:  #7A3A2C;
  /* Base accent = the default (atelier/dark) preset, so --accent is NEVER undefined even
     for one frame. An undefined custom property inherits `initial`, which paints accent
     text as unstyled — visible, wrong, and easy to miss in review. */
  --accent: #E8B964; --accent-fill: #E8B964; --accent-rgb: 232,185,100;
  --accent-bright: #F0CB80; --accent-dim: #71561E; --focus-ring: #D9A648;
  --panel-danger-bg: #1E1310;  --panel-danger-border: #6B3021; --panel-danger-fg: #F0A895;
}

:root[data-theme="light"] {
  color-scheme: light;
  --surface:             #F4EFE4;
  --surface-card:        #FBF8F0;
  --surface-inset:       #EAE3D3;
  --surface-active:      #DCE8DE;
  --surface-accent:      #C6DACB;
  --surface-veil:        rgba(244,239,228,.94);
  --surface-veil-strong: rgba(244,239,228,.86);
  --surface-spoiler:     rgba(198,218,203,.55);

  /* Android light ladder (values/themes.xml). lowest and bright are the same #FFFDF7 there. */
  --surface-lowest:      #FFFDF7;
  --surface-base:        #FCF8F0;
  --surface-container:   #F6EFE0;
  --surface-highest:     #ECE1C9;
  --surface-bright:      #FFFDF7;

  --text:            #1B211A;
  --text-rgb:        27,33,26;
  --text-body:       #2A3128;
  --text-body-dim:   #454C42;
  --text-soft:       #2F3A2C;
  --text-muted:      #2A5536;
  --text-dim:        #5B6656;

  --hairline-rgb:    20,42,27;

  --on-accent:       #0A100E;
  --danger:          #A83A24;
  --danger-rgb:      168,58,36;
  --danger-bright:   #8E2E1B;

  --field-bg:        #FFFFFF;
  --field-fg:        #1B211A;
  --alert-ok-bg:     #EAF1ED;  --alert-ok-fg:   #2A5536;  --alert-ok-border:   #C6DACB;
  --alert-err-bg:    #FBEDE9;  --alert-err-fg:  #823522;  --alert-err-border:  #F2C9BC;
  --panel-danger-bg: #FDF6F3;  --panel-danger-border: #E8C4B8; --panel-danger-fg: #823522;
}

/* ── Accent presets ──────────────────────────────────────────────────────────
   These mirror ThemePresets.kt so a theme picked on the phone is recognisable here.
   Android expands the seed through Material Color Utilities into a full tonal palette;
   the web cannot reproduce MCU, so each preset instead carries two HAND-PICKED accent
   tones chosen for contrast against their ground. It is an approximation of the app's
   scheme, not a match — the seed (--accent-fill) is exact for six of the eight, and the derived
   tones never are.

   🐛 FOREST AND TEAL ARE DELIBERATELY DARKER THAN THEIR ANDROID SEEDS. --accent-fill is not just a
   swatch: 15 rules paint it as a BACKGROUND carrying --on-accent text (.btn-solid, .tab.active,
   .fab, .segmented input:checked+label, .switch:checked, badges …). At the shipped seeds the
   16px/600 label failed AA in BOTH themes — forest #3F7D4F gave 4.30:1 and teal #2F8E8A gave
   3.42:1 against #F4EFE4, and these declarations carry no theme qualifier so there was no ground
   on which they passed. Darkened to 5.63:1 and 5.51:1. The other six pass untouched (4.58–10.57).
   The theme picker is unaffected and still shows the TRUE Android seed: account.js buildSwatches()
   paints .swatch-dot from its own PRESET_SEEDS map, not from this token. */
:root[data-accent="atelier"] { --accent-fill: #E8B964; --accent-rgb: 232,185,100; --on-accent: #0A100E; }
:root[data-accent="forest"]  { --accent-fill: #356943; --accent-rgb: 53,105,67;   --on-accent: #F4EFE4; }
:root[data-accent="oxblood"] { --accent-fill: #9B2D24; --accent-rgb: 155,45,36;   --on-accent: #F4EFE4; }
:root[data-accent="rose"]    { --accent-fill: #C76B82; --accent-rgb: 199,107,130; --on-accent: #1B211A; }
:root[data-accent="plum"]    { --accent-fill: #7E5A9E; --accent-rgb: 126,90,158;  --on-accent: #F4EFE4; }
:root[data-accent="indigo"]  { --accent-fill: #4E6E8E; --accent-rgb: 78,110,142;  --on-accent: #F4EFE4; }
:root[data-accent="teal"]    { --accent-fill: #226A67; --accent-rgb: 34,106,103;  --on-accent: #F4EFE4; }
:root[data-accent="ember"]   { --accent-fill: #D9A648; --accent-rgb: 217,166,72;  --on-accent: #1B211A; }

:root[data-theme="dark"][data-accent="atelier"] { --accent: #E8B964; --accent-bright: #F0CB80; --accent-dim: #71561E; --focus-ring: #D9A648; }
:root[data-theme="dark"][data-accent="forest"]  { --accent: #6FAF7E; --accent-bright: #8FC79C; --accent-dim: #24462E; --focus-ring: #6FAF7E; }
:root[data-theme="dark"][data-accent="oxblood"] { --accent: #E08076; --accent-bright: #EFA096; --accent-dim: #5A1A15; --focus-ring: #E08076; }
:root[data-theme="dark"][data-accent="rose"]    { --accent: #E9A3B3; --accent-bright: #F4BFCA; --accent-dim: #6E3543; --focus-ring: #E9A3B3; }
:root[data-theme="dark"][data-accent="plum"]    { --accent: #BFA0D8; --accent-bright: #D3BCE7; --accent-dim: #46325A; --focus-ring: #BFA0D8; }
:root[data-theme="dark"][data-accent="indigo"]  { --accent: #93B4D2; --accent-bright: #B0CBE3; --accent-dim: #2C3F52; --focus-ring: #93B4D2; }
:root[data-theme="dark"][data-accent="teal"]    { --accent: #63C4BF; --accent-bright: #8BD8D4; --accent-dim: #1B4E4C; --focus-ring: #63C4BF; }
:root[data-theme="dark"][data-accent="ember"]   { --accent: #EFC172; --accent-bright: #F7D394; --accent-dim: #6B520F; --focus-ring: #EFC172; }

:root[data-theme="light"][data-accent="atelier"] { --accent: #8A6220; --accent-bright: #6F4E17; --accent-dim: #C9A96F; --focus-ring: #8A6220; }
:root[data-theme="light"][data-accent="forest"]  { --accent: #2C5C39; --accent-bright: #1E4527; --accent-dim: #9DBFA6; --focus-ring: #2C5C39; }
:root[data-theme="light"][data-accent="oxblood"] { --accent: #8A2820; --accent-bright: #6C1E17; --accent-dim: #D6A29C; --focus-ring: #8A2820; }
:root[data-theme="light"][data-accent="rose"]    { --accent: #9C3F57; --accent-bright: #7D2E43; --accent-dim: #E3B4C0; --focus-ring: #9C3F57; }
:root[data-theme="light"][data-accent="plum"]    { --accent: #6A4788; --accent-bright: #53356D; --accent-dim: #C6B0D8; --focus-ring: #6A4788; }
:root[data-theme="light"][data-accent="indigo"]  { --accent: #3A5670; --accent-bright: #2A4157; --accent-dim: #AEC3D6; --focus-ring: #3A5670; }
:root[data-theme="light"][data-accent="teal"]    { --accent: #1F6B67; --accent-bright: #14524F; --accent-dim: #A3CFCC; --focus-ring: #1F6B67; }
:root[data-theme="light"][data-accent="ember"]   { --accent: #7E5C13; --accent-bright: #63480C; --accent-dim: #E0C68A; --focus-ring: #7E5C13; }

/* Fallback: no data-accent yet (theme.js not run, or an unknown preset from a newer
   app build) — keep the brand amber rather than leaving --accent undefined, which
   would collapse to `initial` and render accent text as transparent-ish inherit.
   `custom` is listed for the same reason: theme.js only stamps it alongside the inline
   vars derived from the reader's seed, and those inline styles out-specify this rule —
   but if it ever appears without them, this is what keeps the page legible. */
:root:not([data-accent]), :root[data-accent="custom"] {
  --accent: #E8B964; --accent-fill: #E8B964; --accent-rgb: 232,185,100;
  --accent-bright: #F0CB80; --accent-dim: #71561E; --focus-ring: #D9A648; --on-accent: #0A100E;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--parchment);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--forest-700); text-underline-offset: 2px; }
/* 🐛 TWO BUGS IN FOUR LINES, and this is the ONLY focus rule for ordinary links and buttons on
   the whole site.
   1. It outlined in --amber-400, a BRAND token that never participates in the theme or accent
      system. #D9A648 is 1.93:1 on --parchment and 2.08:1 on the .auth-card/.panel #FBF8F0 --
      under WCAG 1.4.11's 3:1 floor for a non-text UI indicator. The correct token, --focus-ring,
      already exists and is defined on every :root branch including the no-accent fallback.
   2. `border-radius: 4px` was styling the ELEMENT, not the outline. At (0,1,1) it out-specifies
      `.btn { border-radius: 999px }` at (0,1,0), so focusing a pill button visibly squared its
      corners. It is also unnecessary: browsers already follow the element's own radius. */
a:focus-visible, button:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}

/* The landing page is a FIXED two-zone design, not a themed surface: parchment body with dark
   hero / pledge / footer bands, and it loads no theme.js. One --focus-ring cannot serve both
   grounds -- amber is 8.69:1 on the dark bands but 1.93:1 on parchment, and the dark amber that
   fixes parchment (4.76:1) drops to 2.80:1 on .pledge. So scope it per zone. */
body.marketing {
  --focus-ring:     #8A6220;
  --ghost-ink:      #2A5536;              /* 7.47:1 on parchment */
  /* .55 not .38: a ghost button has no background, so its BORDER is the component boundary and
     owes WCAG 1.4.11's 3:1. At .38 it composited to 2.22:1 on parchment. .55 gives 3.46:1. */
  --ghost-edge:     rgba(20,42,27,.55);
  --ghost-ink-hover:#8A6220;              /* 4.76:1 on parchment */
}
body.marketing .hero,
body.marketing .pledge,
body.marketing .site-foot {
  --focus-ring:     #E8B964;
  --ghost-ink:      #F4EFE4;
  --ghost-edge:     rgba(244,239,228,.55);   /* was .32 -> 2.64:1; now 5.58:1 on the hero */
  --ghost-ink-hover:#E8B964;
}

/* The page shell. Was 1080px, which left a desktop browser mostly empty gutter. 1280 uses the
   real estate without letting body copy run past a readable measure — the individual text blocks
   below keep their own ch-based caps for that. */
.wrap { max-width: 1280px; margin: 0 auto; padding-inline: var(--pad); }

/* ── Header ─────────────────────────────────────────────────────────────── */
.site-head {
  position: absolute; inset-inline: 0; top: 0; z-index: 3;
  padding-block: 22px;
}
.site-head .wrap { display: flex; align-items: center; gap: 18px; }
.mark {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--serif); font-size: 21px; font-weight: 600;
  color: var(--parchment); text-decoration: none; letter-spacing: .01em;
}
.mark svg { width: 26px; height: 26px; flex: none; }
.site-head nav { margin-left: auto; display: flex; gap: 22px; align-items: center; }
.site-head nav a {
  color: var(--forest-100); text-decoration: none; font-size: 14.5px; font-weight: 500;
}
.site-head nav a:hover { color: var(--amber-300); }

/* ── Hero — the lamp-lit reading room ───────────────────────────────────── */
.hero {
  position: relative;
  background:
    radial-gradient(120% 90% at 50% -10%, rgba(232,185,100,.20), transparent 58%),
    linear-gradient(180deg, var(--midnight-900) 0%, var(--forest-900) 62%, var(--forest-800) 100%);
  color: var(--parchment);
  padding-block: clamp(120px, 20vh, 190px) 0;
  overflow: hidden;
}
/* 🐛 THIS WAS THE "made for mobile" BUG. It read "max-width: 40ch", and ch resolves against THIS
   element's font-size — body text, ~17px — not against the h1 it contains. 40ch was therefore
   ~350px, so a 76px display headline wrapped at phone width even on a 2560px monitor.
   ⚠️ Never express a DISPLAY block's width in ch of its own, smaller font. The h1 gets room here;
   .sub below keeps its own ch cap, which is where a reading measure genuinely belongs. */
.hero-copy { text-align: center; max-width: min(1040px, 100%); margin-inline: auto; }
.hero h1 {
  font-family: var(--serif); font-weight: 600;
  font-size: clamp(42px, 7.4vw, 88px); line-height: 1.02; margin: 0;
  letter-spacing: -.015em; text-wrap: balance;
}
.hero .tagline {
  font-family: var(--serif); font-style: italic;
  font-size: clamp(19px, 3vw, 25px); color: var(--amber-300);
  margin: 14px 0 0; opacity: .95;
}
.hero .sub {
  color: var(--forest-100); margin: 20px auto 0; max-width: 62ch;
  font-size: clamp(15.5px, 2vw, 17px); line-height: 1.6;
}

.cta-row {
  display: flex; flex-wrap: wrap; gap: 14px; justify-content: center;
  margin-top: 34px;
}
.btn {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 13px 26px; border-radius: 999px;
  font-size: 15.5px; font-weight: 650; text-decoration: none;
  border: 1px solid transparent; cursor: pointer;
  transition: transform .16s ease, box-shadow .16s ease, background-color .16s ease;
}
@media (prefers-reduced-motion: reduce) { .btn { transition: none; } }
.btn-primary {
  background: var(--amber-300); color: var(--midnight-900);
  box-shadow: 0 10px 30px -12px rgba(232,185,100,.7);
}
.btn-primary:hover { background: #F0CB80; transform: translateY(-1px); }
/* 🐛 WAS `color: var(--parchment)` + a near-white border, full stop. That is right for the DARK
   hero it was designed in, and invisible in the closer section, which sits on the parchment body:
   #F4EFE4 text on an #F4EFE4 ground is 1:1. The button only appeared on hover, when it flipped to
   amber -- and amber on parchment is 1.58:1, so even the hover state was barely there.
   Same two-zone problem as --focus-ring above, so same treatment: drive it from tokens that the
   dark bands re-point. Do not put a literal colour back in here. */
.btn-ghost {
  background: transparent;
  color: var(--ghost-ink);
  border-color: var(--ghost-edge);
}
.btn-ghost:hover { border-color: var(--ghost-ink-hover); color: var(--ghost-ink-hover); }
.cta-note {
  margin: 16px 0 0; font-size: 13.5px; color: var(--forest-200);
}

/* Shelf of spines — echoes the app's login screen. Pure CSS, no images. */
.shelf {
  display: flex; align-items: flex-end; justify-content: center;
  gap: 10px; margin: clamp(48px, 8vh, 84px) auto 0;
  padding-inline: var(--pad); max-width: 940px;
  transform: translateY(1px);
}
.spine {
  /* The spines — not .shelf's max-width — are what set the shelf's real width, so raising the
     .shelf cap alone changed nothing. The ceiling scales with the hero it sits under. */
  width: clamp(28px, 5.4vw, 76px);
  height: calc(var(--h) * var(--shelf-scale));
  border-radius: 4px 4px 0 0;
  background: linear-gradient(180deg, var(--sp, #2A5536), color-mix(in srgb, var(--sp, #2A5536) 72%, black));
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.25), 0 -2px 18px -6px rgba(232,185,100,.35);
  display: flex; align-items: center; justify-content: center;
  position: relative;
}
.spine span {
  writing-mode: vertical-rl; transform: rotate(180deg);
  font-family: var(--serif); font-size: 12.5px; letter-spacing: .06em;
  color: rgba(244,239,228,.62); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-height: 82%;
}
.spine::after { /* gilt line */
  content: ""; position: absolute; inset-inline: 5px; top: 12px; height: 1px;
  background: rgba(232,185,100,.4);
}

/* ── Section scaffolding ────────────────────────────────────────────────── */
/* ⚠️ SCOPED AWAY FROM THE APP. This is the MARKETING page rhythm - generous bands between landing
   sections - but app.html loads this sheet for the tokens and the font, and a bare `section`
   selector reaches every <section> the SPA renders. The web client emits 22 different section
   classes; each one silently gained 56-96px of vertical padding it never asked for. On Explore
   that put 96px above AND below all 14 rails - 224px of dead space between one rail's last cover
   and the next rail's heading, against 20dp in the app (item_discover_rail.xml).

   `:not(.client)` rather than `.marketing`, deliberately: the account and legal pages are
   document-shaped too and keep this rhythm exactly as it is today. Only the app opts out. */
body:not(.client) section { padding-block: clamp(56px, 9vw, 96px); }
/* Same trap as .hero-copy, milder: this was var(--measure) (68ch ≈ 623px), which is a READING
   measure, and it was the binding constraint on a 40px h2 — every section heading broke to two
   lines. The heading gets display width here; .lede below keeps the reading measure, which is
   the element that actually wants one. */
.section-head { max-width: 860px; }
.eyebrow {
  font-size: 12px; letter-spacing: .18em; text-transform: uppercase;
  font-weight: 700; color: var(--amber-700); margin: 0 0 12px;
}
h2 {
  font-family: var(--serif); font-weight: 600;
  font-size: clamp(28px, 4.4vw, 40px); line-height: 1.12;
  margin: 0 0 14px; letter-spacing: -.01em; text-wrap: balance;
}
/* ch is correct HERE — this is body copy, and the cap is expressed in its own font. */
.lede { font-size: clamp(16.5px, 2.2vw, 19px); color: var(--ink-soft); margin: 0; max-width: var(--measure); }

/* ── Feature grid ───────────────────────────────────────────────────────── */
.features {
  display: grid; gap: 20px; margin-top: 44px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
/* The card count is FIXED at six, so pin the columns instead of letting auto-fit guess. Every
   minmax() that yields 3 columns at 1200px also yields 4 — a ragged 4 + 2 — somewhere above it,
   and a larger minmax drops to 2 columns around 1024. Neither is expressible as one auto-fit. */
@media (min-width: 940px) { .features { grid-template-columns: repeat(3, 1fr); } }
.card {
  background: #FBF8F0;
  border: 1px solid var(--parchment-dim);
  border-radius: 14px;
  padding: 26px 24px 24px;
  box-shadow: 0 1px 2px rgba(27,33,26,.04), 0 14px 34px -26px rgba(27,33,26,.4);
}
.card .icon {
  width: 38px; height: 38px; border-radius: 10px;
  background: var(--forest-900); color: var(--amber-300);
  display: grid; place-items: center; margin-bottom: 16px;
}
.card .icon svg { width: 20px; height: 20px; }
.card h3 {
  font-family: var(--serif); font-size: 20px; font-weight: 600;
  margin: 0 0 8px; letter-spacing: -.005em;
}
.card p { margin: 0; font-size: 15.5px; color: var(--ink-soft); line-height: 1.6; }

/* ── The "quiet" pledge band ────────────────────────────────────────────── */
.pledge {
  background: var(--forest-900);
  color: var(--parchment);
  background-image: radial-gradient(90% 120% at 88% 0%, rgba(232,185,100,.13), transparent 60%);
}
.pledge h2 { color: var(--parchment); }
.pledge .lede { color: var(--forest-100); }
.pledge-list {
  list-style: none; margin: 36px 0 0; padding: 0;
  /* Four pledges: 1, then 2 + 2, then a single row of 4. auto-fit's in-between state was a
     ragged 3 + 1 — same fixed-count problem as .features. */
  display: grid; gap: 14px; grid-template-columns: 1fr;
}
@media (min-width: 620px)  { .pledge-list { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .pledge-list { grid-template-columns: repeat(4, 1fr); } }
.pledge-list li {
  display: flex; gap: 12px; align-items: flex-start;
  font-size: 15.5px; color: var(--forest-100); line-height: 1.55;
}
.pledge-list svg { width: 19px; height: 19px; flex: none; color: var(--amber-300); margin-top: 2px; }
.pledge-list b { color: var(--parchment); font-weight: 650; }

/* ── Stats ──────────────────────────────────────────────────────────────── */
.stats {
  display: grid; gap: 18px; margin-top: 40px;
  /* Three stats: one row, or stacked. auto-fit's middle state here was a ragged 2 + 1. */
  grid-template-columns: 1fr;
}
@media (min-width: 700px) { .stats { grid-template-columns: repeat(3, 1fr); } }
.stat {
  border-top: 2px solid var(--amber-400);
  padding-top: 16px;
}
.stat .n {
  font-family: var(--serif); font-size: clamp(30px, 5vw, 42px); font-weight: 600;
  line-height: 1; display: block; font-variant-numeric: tabular-nums;
}
.stat .l { font-size: 14px; color: var(--ink-muted); margin-top: 7px; display: block; }
/* The only .stats block on the site sits inside .pledge, on --forest-900. --ink-muted against that
   is ~3.2:1 — under AA for 14px text. .pledge .lede already overrides for the same reason. */
.pledge .stat .l { color: var(--forest-100); }

/* ── Closing CTA ────────────────────────────────────────────────────────── */
.closer { text-align: center; }
.closer h2 { margin-inline: auto; max-width: 30ch; }
.closer .lede { margin-inline: auto; }
.closer .cta-row { margin-top: 30px; }

/* ── Footer ─────────────────────────────────────────────────────────────── */
.site-foot {
  background: var(--midnight-900); color: var(--forest-200);
  padding-block: 46px 40px; font-size: 14px;
}
.foot-grid {
  display: flex; flex-wrap: wrap; gap: 28px 48px; align-items: flex-start;
}
.foot-brand { display: flex; flex-direction: column; gap: 10px; max-width: 34ch; }
.foot-brand .mark { font-size: 19px; }
.foot-brand p { margin: 0; color: var(--forest-200); font-size: 14px; line-height: 1.55; }
.foot-links { display: flex; gap: 44px; margin-left: auto; flex-wrap: wrap; }
.foot-links div { display: flex; flex-direction: column; gap: 9px; }
.foot-links h4 {
  margin: 0 0 4px; font-size: 11.5px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--forest-500); font-weight: 700;
}
.foot-links a { color: var(--forest-100); text-decoration: none; }
.foot-links a:hover { color: var(--amber-300); text-decoration: underline; }
.foot-base {
  margin-top: 34px; padding-top: 20px;
  border-top: 1px solid rgba(160,192,167,.14);
  display: flex; flex-wrap: wrap; gap: 10px 20px; align-items: center;
  color: var(--forest-500); font-size: 13px;
}

@media (max-width: 640px) {
  /* display:none took the WHOLE nav away below 640px with no replacement, so a phone visitor had
     no Sign in link anywhere above the fold - the one control a returning reader looks for. Hide
     only the in-page anchors (Features / Why PageLab are reachable by scrolling) and keep the
     links that actually go somewhere. */
  .site-head nav a[href^="#"] { display: none; }

  /* 🐛 THE HEADER RAN OFF THE SIDE OF SMALL PHONES. Keeping three real links was right, but
     nothing was made narrower to pay for them: the brand alone is 124px at 21px type, and
     brand + Open PageLab + Download + Sign in + gaps came to 361px inside a 341px viewport.
     `.site-head .wrap` is flex with the default `nowrap` and nothing may shrink, so the row did
     not compress - it overflowed its own `padding-inline`, put "Sign in" hard against the screen
     edge with a 0px gutter, and made the whole DOCUMENT scroll sideways. Measured on a 320px
     viewport; 375px happened to fit, which is why it looked fine on the common test size.

     Fixed by making the row cheaper rather than by dropping a link or wrapping to two rows:
     smaller brand type and mark, and tighter gaps. That buys ~40px, which clears 320px with the
     full 20px gutter intact on both sides and leaves all three destinations reachable.
     `min-width: 0` on the mark is the backstop - a flex item defaults to `min-width: auto` and
     therefore REFUSES to shrink below its content, which is the mechanism that turned a tight
     fit into an overflow in the first place. */
  .site-head .wrap { gap: 10px; }
  .site-head nav { gap: 10px; font-size: 14px; }
  .mark { font-size: 17px; min-width: 0; }
  .mark svg { width: 22px; height: 22px; }

  /* ...and they were 24px tall, at EVERY width, not just the ones that overflowed. A 14.5px text
     link is 24px of hit area against a 44px minimum, so the three controls in the header were the
     hardest things on the marketing site to tap. inline-flex because a plain inline <a> ignores
     min-height. This does not change the header height: 44px still fits inside the existing
     22px padding-block, measured at 92px before and after. */
  .site-head nav a { min-height: 44px; display: inline-flex; align-items: center; }
  .foot-links { margin-left: 0; gap: 30px; }
}

/* ── Literata ────────────────────────────────────────────────────────────────────────────────
   The brand serif. It ships in the Android APK (app/src/main/res/font/literata.ttf) and until
   2026-08-19 had NO @font-face here and no font file under public/ at all, so every serif on the
   web silently fell through to Iowan Old Style / Palatino. That included the CANVAS-RENDERED book
   covers, which meant one saved cover rendered as two different pictures across the two clients.

   Variable font: one file spans weight 200-900, and the `opsz` axis (7-72) is applied by the
   browser automatically via font-optical-sizing. Declaring the range on font-weight is what stops
   the browser synthesising a fake bold.

   font-display: swap - text paints immediately in the fallback and re-renders when the face
   arrives. Never `block`: a 388K face on a cold cache would otherwise hold the first paint.

   ⚠️ Requires `fontSrc` in the site CSP (server/src/index.js). default-src is 'none', so without
   it the browser blocks the download and reports nothing that looks like an error.
   Licence: SIL OFL 1.1 — see fonts/OFL.txt, which must ship alongside the files. */
@font-face {
  font-family: 'Literata';
  src: url('/site/fonts/literata.woff2') format('woff2');
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Literata';
  src: url('/site/fonts/literata-italic.woff2') format('woff2');
  font-weight: 200 900;
  font-style: italic;
  font-display: swap;
}
