/* ---------------------------------------------------------------------
   Lamplight, as shipped.

   Every value here is the app's own token (app/lib/core/design/tokens.dart,
   restated in docs/brand-guidelines.md section 9). The site and the product
   are the same object; a landing page in near-enough colours is how a brand
   starts drifting.

   Three theme states, not two: an explicit choice stamps data-theme on the
   root, and the default stamps nothing, so the un-stamped document has to
   resolve correctly on its own.
   --------------------------------------------------------------------- */
:root {
  --paper:    #F6EFE0;
  --surface:  #FFFBF2;
  --surface2: #EDE3D0;
  --ink:      #33291F;
  --soft:     #5C4E3E;
  --muted:    #786A57;
  --amber:    #9A5E1C;
  --honey:    #CEA241;
  --sage:     #7C8B5A;
  --slate:    #7B899E;

  /* How opaque the mark's cone is. The same light needs two alphas to read
     as light, because normal blending is not additive: amber at 0.18 over
     cream is a warm haze, but the night amber at that alpha over #030E1A is
     a muddy wedge that reads as a shadow under the lamp. Lifted with the
     rest of the mark from app/lib/core/design/brand_mark.dart. */
  --cone: 0.18;

  /* The beam. Honey at low alpha over paper, never a grey shadow. */
  --beam-near: rgba(206, 162, 65, 0.22);
  --beam-far:  rgba(206, 162, 65, 0);
  --rule:      rgba(51, 41, 31, 0.14);
  --ring:      rgba(51, 41, 31, 0.10);
  --lift:      0 1px 2px rgba(51, 41, 31, 0.05), 0 8px 24px rgba(51, 41, 31, 0.06);

  /* Which of a day/night image pair is the one on screen. Two tokens rather
     than a repeated pair of selectors, so the swap is declared once here and
     once in each night block, next to the colours it belongs with. */
  --day-only:   block;
  --night-only: none;

  /* The hero's device frames: a warm near-black bezel, like the ink, and a
     deeper lift than a card's, because a device stands off the page rather
     than lying on it. */
  --bezel:         #2A2119;
  --device-shadow: 0 2px 4px rgba(51, 41, 31, 0.08), 0 28px 48px -16px rgba(51, 41, 31, 0.30);

  /* The category hues are the app's own, and the night set is derived from
     the day set exactly as `VunaliColors.categoryHue()` derives it: +0.16
     lightness in HSL, not a flat step on each RGB channel. The two produce
     visibly different colours, and only one of them is what a parent sees
     when they put the phone down and pick the site up. */
  --cat-bedtime:    #6B5E8C;
  --cat-brave:      #B0503A;
  --cat-funny:      #C08A2E;
  --cat-animals:    #4F7D5E;
  --cat-veryshort:  #5E7D9E;
  --cat-science:    #3B7D8C;
  --cat-fantasy:    #8A5896;
  --cat-feelings:   #B36F78;
  --cat-professions:#9E6034;
  --cat-nature:     #6E8B4F;

  --display: "Petrona", Georgia, "Times New Roman", serif;
  --body:    "Literata", Georgia, "Times New Roman", serif;
  --ui:      "Figtree", system-ui, -apple-system, "Segoe UI", sans-serif;

  --measure: 34rem;
  --page:    72rem;

  /* Tells the browser which way its own painting goes: form controls, the
     scrollbar, autofill, the flash of canvas before the stylesheet lands.
     Without it the notify form's email field renders as a light native
     input on a night-blue page, which is the one part of a page a
     stylesheet cannot reach. */
  color-scheme: light;
}

/* An explicit choice of day on a device set to night. The tokens are
   already right, because the night block below excludes a stamped light
   root; this is only here so the browser's own painting follows too. */
:root[data-theme="light"] { color-scheme: light; }

/* Night. The room lights going down, not a preference toggle: the app
   changes colour only, over 400ms, and so does this. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --paper:    #030E1A;
    --surface:  #0B1726;
    --surface2: #142334;
    --ink:      #EFE3CC;
    --soft:     #D2C3A6;
    --muted:    #9C8D77;
    --amber:    #E0A155;
    --honey:    #E9C079;
    --sage:     #93A472;
    --slate:    #8FA3BC;
    --cone: 0.55;

    --beam-near: rgba(233, 192, 121, 0.16);
    --beam-far:  rgba(233, 192, 121, 0);
    --rule:      rgba(239, 227, 204, 0.16);
    --ring:      rgba(239, 227, 204, 0.14);
    /* No shadow at night. The app drops every elevation shadow in the night
       palette and separates raised surfaces with the hairline ring alone,
       because a drop shadow on a near-black ground reads as a smudge rather
       than as height. The ring above is doing the work. */
    --lift:      none;

    --day-only:   none;
    --night-only: block;

    /* Lighter than the paper at night, so the frame reads against it, and
       no shadow, for the reason --lift has none. */
    --bezel:         #1A2636;
    --device-shadow: none;

    --cat-bedtime:    #958BB1;
    --cat-brave:      #CF7F6D;
    --cat-funny:      #DAAF65;
    --cat-animals:    #76A886;
    --cat-veryshort:  #90A6BD;
    --cat-science:    #5EAABB;
    --cat-fantasy:    #AF87B9;
    --cat-feelings:   #CFA4AA;
    --cat-professions:#C9885B;
    --cat-nature:     #97B379;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --paper:    #030E1A;
  --surface:  #0B1726;
  --surface2: #142334;
  --ink:      #EFE3CC;
  --soft:     #D2C3A6;
  --muted:    #9C8D77;
  --amber:    #E0A155;
  --honey:    #E9C079;
  --sage:     #93A472;
  --slate:    #8FA3BC;
  --cone: 0.55;

  --beam-near: rgba(233, 192, 121, 0.16);
  --beam-far:  rgba(233, 192, 121, 0);
  --rule:      rgba(239, 227, 204, 0.16);
  --ring:      rgba(239, 227, 204, 0.14);
  --lift:      none;

  --day-only:   none;
  --night-only: block;

  --bezel:         #1A2636;
  --device-shadow: none;

  --cat-bedtime:    #958BB1;
  --cat-brave:      #CF7F6D;
  --cat-funny:      #DAAF65;
  --cat-animals:    #76A886;
  --cat-veryshort:  #90A6BD;
  --cat-science:    #5EAABB;
  --cat-fantasy:    #AF87B9;
  --cat-feelings:   #CFA4AA;
  --cat-professions:#C9885B;
  --cat-nature:     #97B379;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-optical-sizing: auto;
  font-size: 1.0625rem;
  line-height: 1.65;
  transition: background-color 400ms ease, color 400ms ease;
}

/* The app's bedtime change is colour-only and 400ms. Matching it here
   means the control below demonstrates the real thing rather than a
   website's idea of it. */
body * { transition: background-color 400ms ease, color 400ms ease, border-color 400ms ease; }

@media (prefers-reduced-motion: reduce) {
  body, body * { transition-duration: 1ms !important; animation: none !important; }
}

h1, h2, h3 {
  font-family: var(--display);
  font-variation-settings: "WONK" 0, "SOFT" 0;
  font-weight: 500;
  line-height: 1.12;
  text-wrap: balance;
  margin: 0;
}

p { margin: 0; }

a { color: inherit; }

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 3px;
  border-radius: 4px;
}

.wrap {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 5vw, 2.5rem);
}

/* --- Masthead ------------------------------------------------------- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 10;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid transparent;
}

.masthead.is-scrolled { border-bottom-color: var(--rule); }

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.875rem;
}

.wordmark {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--display);
  font-variation-settings: "WONK" 0;
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
}

/* `flex: none` because .wordmark is a flex row: without it the plate is a
   flex item free to shrink, and it loses its aspect ratio before the text
   beside it gives up any width. */
.lamp { display: block; flex: none; }

/* The painted plates, in the masthead of every edition, beside the
   wordmark. They were briefly in the hero instead, at ~100px, on a
   misreading of which of the two the owner wanted gone -- two of the same
   drawing 200px apart was the problem, and the hero is the one that went.
 *
 * Both plates are in the markup and exactly one is shown, in each of the
 * three theme states this stylesheet already distinguishes. Not a script
 * swapping `src`: the theme is resolved before first paint by the bootstrap
 * in <head>, and a mark that arrives a frame later is a masthead that
 * visibly assembles itself.
 *
 * The night plate is a different painting, not a filtered one -- the book
 * is repainted toward night's ink and the lamp is left alone, because a
 * lamp is warm at any hour. See scripts/build-site-media.py. */
.lamp--night { display: none; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .lamp--day   { display: none; }
  :root:not([data-theme="light"]) .lamp--night { display: block; }
}

/* After the media query, so an explicit choice wins in either direction:
   dark chosen on a light system lands here, and light chosen on a dark one
   is already excluded by the :not() above. */
:root[data-theme="dark"] .lamp--day   { display: none; }
:root[data-theme="dark"] .lamp--night { display: block; }

/* Masthead size. The intrinsic 30x35 in the markup is what the browser
   reserves before the plate arrives, so the box does not move; this only
   ties the height to the wordmark's own type size, which is what keeps the
   lockup aligned when the display face is swapped or the root size
   changes.

   Worth knowing what is given up at this size: the lamp's light on the
   pages, which is the idea of the drawing, is largely gone below about
   5rem and what survives is the silhouette. That was the argument for
   putting it in the hero instead. The masthead is where it goes. */
.wordmark .lamp {
  height: 1.35em;
  width: auto;
}

/* The mark's two colours, resolved from the theme rather than fixed, for
   the reason the app gives: the body follows `ink` and inverts, while the
   light stays warm at any hour. The pages are not a third colour, they are
   the ground with the lamp's own warmth laid back over them.
 *
 * These four still matter: /delete-account/ and the five legal pages draw
 * the flat vector inline, the same way every screen in the app other than
 * the shelf masthead does. */
.lamp__page  { fill: var(--paper); }
.lamp__body  { fill: var(--ink); }
.lamp__light { fill: var(--amber); }
.lamp__lit   { fill: var(--amber); fill-opacity: var(--cone); }


/* --- the hero devices ------------------------------------------------

   The app itself, on the two screens it is read on: an iPad at home, and
   a phone in front of it open at the first page of a tale. Both screens
   are real captures of the shipped app (mockup_capture_test.dart, the
   `hero-*` captures); the frames are drawn here rather than baked into the
   files, so they follow the theme with everything else.

   Laid out in one box of fixed proportion, sized off its own width with
   container units, so the pair keeps its composition from a 20rem phone
   column to the desktop one instead of re-flowing into two loose images.
   The tablet stands at the back right; the phone overlaps its lower left
   corner and sits a little lower, the way two devices on a table do.
   ------------------------------------------------------------------- */

.hero__devices {
  position: relative;
  justify-self: center;
  width: min(30rem, 100%);
  /* Tablet frame 1.121 widths tall, and the phone standing 0.06 below it. */
  aspect-ratio: 50 / 59;
  margin: 0;
  container-type: inline-size;
}

.device {
  position: absolute;
  background: var(--bezel);
  box-shadow: 0 0 0 1px var(--ring), var(--device-shadow);
}

.device__screen {
  display: block;
  width: 100%;
  /* Load-bearing, as on the step shots: the width and height attributes
     would otherwise win over the image's own ratio. */
  height: auto;
  /* What the screen shows while the capture is still arriving: the app's
     own paper, so a slow connection sees a device switched on rather than
     a dark slab with nothing in it. */
  background: var(--surface2);
}

.device__screen--day   { display: var(--day-only); }
.device__screen--night { display: var(--night-only); }

.device--tablet {
  top: 0;
  right: 0;
  width: 80%;
  padding: 2.8cqi;
  border-radius: 4.2cqi;
}

.device--tablet .device__screen { border-radius: 1.6cqi; }

.device--phone {
  bottom: 0;
  left: 0;
  width: 36%;
  padding: 1.6cqi;
  border-radius: 6cqi;
}

.device--phone .device__screen { border-radius: 4.6cqi; }

.dimmer {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--ui);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--soft);
  background: var(--surface);
  border: 1px solid var(--ring);
  border-radius: 999px;
  padding: 0.5rem 0.9rem;
  cursor: pointer;
  min-height: 44px;
}

.dimmer:hover { color: var(--ink); border-color: var(--amber); }

.dimmer__dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  background: var(--honey);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--honey) 22%, transparent);
}

/* --- Hero ----------------------------------------------------------- */

.hero {
  position: relative;
  overflow: hidden;
  padding-block: clamp(3.5rem, 11vw, 7.5rem) clamp(3rem, 8vw, 5.5rem);
}

/* The beam. It falls from the top right the way the lamp in the mark
   does, and it is the only decorative element on the page. */
.hero::before {
  content: "";
  position: absolute;
  inset: -20% -10% auto auto;
  width: min(46rem, 90vw);
  aspect-ratio: 1;
  background: radial-gradient(closest-side, var(--beam-near), var(--beam-far));
  pointer-events: none;
}

.hero__inner {
  position: relative;
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
  align-items: center;
}

/* The device only earns a column once the copy still has a comfortable
   measure beside it. Below that it goes under the words, where it is the
   first thing after the buttons rather than a competitor to them. */
@media (min-width: 62rem) {
  .hero__inner { grid-template-columns: minmax(0, 1fr) min(29rem, 42vw); }
}

.hero h1 {
  font-size: clamp(2.5rem, 7vw, 4.75rem);
  /* 500 is the app's Display weight. The 400 that stood here made the one
     line of Petrona the page is built around lighter on the web than it is
     in the product it is selling. */
  font-weight: 500;
  letter-spacing: -0.02em;
  max-width: 18ch;
  margin-block: 1.5rem 0;
}

.hero h1 em {
  font-style: normal;
  font-weight: 600;
  color: var(--amber);
}

.hero__lede {
  max-width: var(--measure);
  margin-top: 1.5rem;
  font-size: 1.1875rem;
  color: var(--soft);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.25rem;
  margin-top: 2.25rem;
}

/* The same row of buttons, closing a page rather than opening one. Used at
   the foot of /delete-account/, where it follows a panel rather than a
   paragraph and needs the extra quarter-rem to clear it. A class, because
   the `style="margin-top: 2.5rem"` it replaces was blocked by the CSP and
   the row sat too close to the panel above it in production. */
.hero__actions--parting { margin-top: 2.5rem; }

.button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 48px;
  padding: 0.75rem 1.5rem;
  border-radius: 999px;
  font-family: var(--ui);
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: none;
  border: 1px solid transparent;
}

.button--primary {
  background: var(--amber);
  color: var(--paper);
  box-shadow: var(--lift);
}

.button--primary:hover { background: color-mix(in srgb, var(--amber) 88%, var(--ink)); }

.button--quiet {
  color: var(--ink);
  border-color: var(--ring);
  background: var(--surface);
}

.button--quiet:hover { border-color: var(--amber); }

.hero__note {
  font-family: var(--ui);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--soft);
  margin-top: 1.5rem;
  display: flex;
  align-items: center;
  gap: 0.625rem;
  flex-wrap: wrap;
}

.hero__note svg { color: var(--amber); flex: none; }
/* The icon and its sentence are one line of meaning, so the sentence wraps
   inside its own column instead of the icon being pushed onto a line by
   itself, which is what happened at phone width. */
.hero__note { flex-wrap: nowrap; align-items: flex-start; }
.hero__note svg { margin-top: 0.15em; }

/* --- The four steps -------------------------------------------------

   A card per step, with its screen flush at the top: `.step__shot` below
   cancels this padding with a negative margin, so the two rules are one
   decision and have to move together. They came apart once already, when
   `.step` went out with a dead `.evening` block and four screenshots ended
   up outside their own cards with a scrollbar under them. */
.step {
  background: var(--surface);
  border: 1px solid var(--ring);
  border-radius: 16px;
  box-shadow: var(--lift);
  overflow: hidden;
  padding: 1.75rem 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.step h3 {
  font-family: var(--ui);
  font-size: 1.0625rem;
  font-weight: 600;
}

.step p { font-size: 0.9375rem; color: var(--soft); }

/* --- A screen at the head of each step ------------------------------ */

.step__shot { margin: -1.75rem -1.5rem 0.5rem; }

.step__shot img {
  display: block;
  width: 100%;
  /* `height: auto` is load-bearing, not tidiness. Every image on this page
     carries width and height attributes so the layout does not jump while
     they load, and that height lands on the height property. A specified
     height wins over aspect-ratio, so without this line the ratio below is
     ignored and the shot renders at its full 1688px. */
  height: auto;
  /* Cropped from the top rather than shown whole. A 1170x2532 screen in a
     quarter-width column is 500px of thumbnail nobody reads; two thirds of
     one is the masthead, the picture and the first line of story, which is
     the part the step is actually about. */
  aspect-ratio: 2 / 3;
  object-fit: cover;
  object-position: top center;
  border-bottom: 1px solid var(--rule);
}

/* --- Sections ------------------------------------------------------- */

.section { padding-block: clamp(3.5rem, 8vw, 6rem); }

.section--ruled { border-top: 1px solid var(--rule); }

.section--tinted { background: var(--surface2); }

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

.section__head h2 {
  font-size: clamp(1.75rem, 3.6vw, 2.5rem);
  margin-top: 0.75rem;
}

.section__head p {
  margin-top: 1rem;
  color: var(--soft);
}

/* --- Categories ----------------------------------------------------- */

.cats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
  margin-top: 2.5rem;
}

.cat {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--ui);
  font-size: 0.875rem;
  font-weight: 600;
  /* Same treatment as `.book__cat`: the hue carried toward the ink so the
     label clears 4.5:1 on its own tint, rather than dropped to grey. The
     chip's background is the raw hue at 14%, so the colour still reads. */
  color: color-mix(in oklab, var(--hue) 45%, var(--ink));
  background: color-mix(in srgb, var(--hue) 14%, transparent);
  border-radius: 12px;
  padding: 0.625rem 1rem;
}

.cat::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 2px;
  background: var(--hue);
}

/* The category hues, as classes rather than as `style="--hue: ..."` on each
   chip.
   ---------------------------------------------------------------------

   This is a security fix wearing a correctness fix's clothes, and both
   halves are real. The pages carried the hue in an inline `style`
   attribute, and the site's CSP sets `style-src 'self' https://fonts
   .googleapis.com` with no `'unsafe-inline'` -- and CSP Level 2 blocks
   inline style *attributes*, not only `<style>` blocks. So every one of
   these chips rendered with `--hue` unset in production: `color: var(--hue)`
   with nothing behind it, on a row of eighteen labels whose whole job is to
   be colour-coded. It looked right in every local preview, because a file
   opened from disk has no CSP at all.

   The obvious repair is to add `'unsafe-inline'` to `style-src`, which
   would trade a working row of chips for the loss of the one directive
   still holding after `script-src` was tightened. These classes are the
   repair that costs nothing.

   Named for the hue rather than for the chip, because two different
   elements read `--hue`: the chips in `.cats` and the category label under
   each cover in `.covers`. One set of ten serves both. */
.hue--bedtime { --hue: var(--cat-bedtime); }
.hue--brave { --hue: var(--cat-brave); }
.hue--funny { --hue: var(--cat-funny); }
.hue--animals { --hue: var(--cat-animals); }
.hue--veryshort { --hue: var(--cat-veryshort); }
.hue--science { --hue: var(--cat-science); }
.hue--fantasy { --hue: var(--cat-fantasy); }
.hue--feelings { --hue: var(--cat-feelings); }
.hue--professions { --hue: var(--cat-professions); }
.hue--nature { --hue: var(--cat-nature); }

/* --- Two-column feature list --------------------------------------- */

.feature { display: flex; flex-direction: column; gap: 0.5rem; }

.feature h3 {
  font-size: 1.25rem;
  font-weight: 600;
  padding-top: 0.9rem;
  border-top: 2px solid var(--amber);
}

.feature p { color: var(--soft); font-size: 0.9375rem; }

/* The section that used a grid of these is down to one, so it takes the
   reading measure rather than a column of a grid that no longer exists. */
.feature--single { margin-top: 2.5rem; max-width: var(--measure); }

/* Two beats side by side once there is room for them: the failure a parent
   has already seen, and the undertaking about it. Stacked, they left the
   right half of the section empty, which is what a column of prose does on
   a wide screen when the grid that used to sit beside it is gone. */
@media (min-width: 62rem) {
  .pictures {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 3.5rem;
    align-items: start;
  }
  .pictures .feature--single { margin-top: 0.75rem; }
}

/* --- The shelf, at a size a parent can judge -------------------------

   Eight covers in a grid rather than eight in a scrolling rail. The rail
   was right when the openings carried the argument and the covers were
   supporting evidence; with the prose off the page these are the only
   thing left that shows craft rather than describing it, so they get the
   room the prose had.

   Four across at the widest, which lands each cover around 15rem: large
   enough that the brushwork reads, small enough that eight of them still
   scan as a shelf rather than as eight separate claims. */
.shelf {
  list-style: none;
  margin: 3rem 0 0;
  padding: 0;
  display: grid;
  gap: 1.5rem 1.25rem;
  grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 40rem) { .shelf { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 62rem) { .shelf { grid-template-columns: repeat(4, 1fr); } }

.book {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.book img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 14px;
  border: 1px solid var(--ring);
  box-shadow: var(--lift);
}

.book__title {
  font-family: var(--display);
  font-variation-settings: "WONK" 0;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.3;
  margin-top: 0.35rem;
}

/* Pushed to the bottom, so a two-line title does not drop its category out
   of line with the one-line title beside it. */
.book__meta {
  margin-top: auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

.book__cat {
  font-family: var(--ui);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* The category hue carried toward the ink rather than used raw. The app's
     hues are tuned for a filled chip, and several of them land near 3.3:1 as
     11px text on paper -- Funny and Feelings are the worst. Mixed with the
     foreground rather than replaced by grey, so the colour still codes the
     category the way the shelf does. The night hues already clear 8:1, and
     the same mix against night's pale ink would wash them out, so this is
     day only. */
  color: color-mix(in oklab, var(--hue) 45%, var(--ink));
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .book__cat { color: var(--hue); }
}

:root[data-theme="dark"] .book__cat { color: var(--hue); }

/* The five free books, marked where a parent is already looking rather than
   only named in the offer three screens down.

   Amber, which is this page's colour for things that are actually here --
   the rule `.soon__mark` sets by refusing it -- and set in the same type as
   the category beside it. A filled sticker would read as a discount flag on
   a store shelf; a free book is not a discount, it is simply the parent's.
   `flex: none` so a long category wraps before this does. */
.book__free {
  flex: none;
  font-family: var(--ui);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--amber);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .cat { color: var(--hue); }
}

:root[data-theme="dark"] .cat { color: var(--hue); }

/* --- The offer -------------------------------------------------------

   Two panels, not a pricing table: there is one product and two ways to
   pay for it, and a comparison grid would invent a decision the parent
   does not have to make.

   The free books take the amber, which this page spends only on things a
   parent can have tonight. Five books they keep are the one thing on it
   that qualifies. */
.offer {
  margin-top: 3rem;
  display: grid;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: 16px;
  overflow: hidden;
}

@media (min-width: 44rem) { .offer { grid-template-columns: 1fr 1fr; } }

.offer__card {
  background: var(--surface);
  padding: 2rem 1.75rem;
}

.offer__price {
  font-family: var(--display);
  font-variation-settings: "WONK" 0;
  font-size: clamp(2.25rem, 6vw, 3rem);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
  margin: 0;
  /* Lining figures, so $7.99 sits on the baseline beside the word Free
     rather than dropping its 7 and 9 below it the way Petrona's default
     old-style figures do. */
  font-variant-numeric: lining-nums tabular-nums;
}

.offer__card:first-child .offer__price { color: var(--amber); }

.offer__term {
  margin: 0.4rem 0 0;
  font-family: var(--ui);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.offer__card p:last-child {
  margin: 1rem 0 0;
  color: var(--soft);
  font-size: 0.9375rem;
}

/* The five, named. Every app on the store offers "some free stories", so an
   unnamed free tier reads as a sample that stops on page one. Five titles a
   parent can go back up the page and find, marked, is a different claim.

   `cite` because they are works, and each one is rewritten from
   tales/<id>/tale.yaml at build time like every other title on the page, so
   this list cannot drift from the shelf above it or from the app. */
.offer__titles {
  list-style: none;
  margin: 1.5rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.4rem;
}

.offer__titles cite {
  font-family: var(--display);
  font-variation-settings: "WONK" 0;
  font-style: normal;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.3;
}

/* --- The steps and the feature rows ----------------------------------

   Both were `.grid` with the layout decided by a second class. Named for
   what they hold instead, because a four-step sequence and a three-item
   feature row want different column counts and always did. */
.steps {
  list-style: none;
  margin: 3rem 0 0;
  padding: 0;
  display: grid;
  gap: 1.5rem;
  counter-reset: step;
}

@media (min-width: 44rem) { .steps { grid-template-columns: 1fr 1fr; } }
@media (min-width: 68rem) { .steps { grid-template-columns: repeat(4, 1fr); } }

/* --- The absence list ---------------------------------------------- */

.absent {
  list-style: none;
  margin: 3rem 0 0;
  padding: 0;
  display: grid;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: 16px;
  overflow: hidden;
}

@media (min-width: 44rem) { .absent { grid-template-columns: 1fr 1fr; } }

.absent li { background: var(--paper); padding: 1.375rem 1.5rem; }

.absent h3 {
  font-family: var(--ui);
  font-size: 0.9375rem;
  font-weight: 600;
}

.absent p {
  font-size: 0.875rem;
  color: var(--muted);
  margin: 0.2rem 0 0;
}

/* --- Pull quote ----------------------------------------------------- */

.creed {
  max-width: 44rem;
  margin-top: 3rem;
  font-family: var(--display);
  font-variation-settings: "WONK" 0;
  font-size: clamp(1.375rem, 3vw, 1.875rem);
  font-weight: 400;
  line-height: 1.35;
  text-wrap: balance;
}

.creed span { color: var(--amber); }

/* --- FAQ ------------------------------------------------------------ */

.faq { margin-top: 2.5rem; max-width: 46rem; }

.faq details {
  border-bottom: 1px solid var(--rule);
  padding-block: 1.125rem;
}

.faq summary {
  cursor: pointer;
  list-style: none;
  font-family: var(--ui);
  font-weight: 600;
  font-size: 1rem;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  align-items: baseline;
  /* 44, not 32. The app holds every tappable thing to 44px and this row is
     the only control on the page that was under it: a one-finger target on
     a phone, in the dark, belonging to the tiredest reader on the site. */
  min-height: 44px;
  align-content: center;
}

.faq summary::-webkit-details-marker { display: none; }

.faq summary::after {
  content: "+";
  color: var(--amber);
  font-size: 1.25rem;
  line-height: 1;
  flex: none;
}

.faq details[open] summary::after { content: "\2013"; }

.faq p {
  margin-top: 0.75rem;
  color: var(--soft);
  font-size: 0.9375rem;
  max-width: var(--measure);
}

/* --- Account deletion ---------------------------------------------- */

.panel {
  margin-top: 2.5rem;
  background: var(--surface);
  border: 1px solid var(--ring);
  border-radius: 16px;
  padding: clamp(1.5rem, 4vw, 2.25rem);
  max-width: 46rem;
}

/* `ul` as well as `ol`. The deletion page's "what deletion removes" list is
   unordered and was carrying this exact declaration block in an inline
   `style` attribute, which the CSP blocks, so it rendered unstyled beside an
   ordered list two panels up that looked correct. One rule, both lists. */
.panel ol,
.panel ul { margin: 1.25rem 0 0; padding-left: 1.25rem; color: var(--soft); font-size: 0.9375rem; }
.panel li { margin-bottom: 0.5rem; }
.panel p { color: var(--soft); font-size: 0.9375rem; }
.panel p + p { margin-top: 0.875rem; }
/* A paragraph after a list wants the same air the list gave itself, and
   `p + p` cannot see across the list to provide it. */
.panel ol + p,
.panel ul + p { margin-top: 1.25rem; }

/* --- Footer --------------------------------------------------------- */

.footer {
  border-top: 1px solid var(--rule);
  padding-block: 3rem 4rem;
  font-family: var(--ui);
  font-size: 0.875rem;
  color: var(--muted);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1.5rem 2rem;
}

.footer nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
}

.footer a { text-decoration: none; }
.footer a:hover { color: var(--ink); }

/* The line under the wordmark in the footer. Was `style="margin-top:0.35rem"`
   on fifteen pages, which the CSP blocks, so the two lines sat flush against
   each other everywhere but a local file:// preview. */
.footer__tagline { margin-top: 0.35rem; }

/* --- The language picker in the masthead ------------------------------

   Nine editions is too many for a row of pills, so the masthead carries a
   disclosure and the footer keeps the flat list. `<details>` is the whole
   control: it opens, closes, and takes focus with no script at all, which
   matters more here than it usually does, because the reader most likely
   to reach for it is the one who cannot read the page it is sitting on.
   Anything that only works once a script has run is the wrong mechanism
   for that reader.
   ------------------------------------------------------------------- */

.masthead__tools {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.langswitch { position: relative; }

.langswitch__button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  padding: 0.5rem 0.75rem;
  font-family: var(--ui);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--soft);
  background: var(--surface);
  border: 1px solid var(--ring);
  border-radius: 999px;
  cursor: pointer;
  /* Both of these, or the disclosure triangle shows: the standard property
     for Firefox and Safari, the pseudo-element for Chrome. */
  list-style: none;
}

.langswitch__button::-webkit-details-marker { display: none; }
.langswitch__button:hover { color: var(--ink); border-color: var(--amber); }
.langswitch[open] > .langswitch__button { color: var(--ink); border-color: var(--amber); }

.langswitch__globe,
.langswitch__chev {
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.langswitch__globe { stroke-width: 4; }
.langswitch__chev { stroke-width: 6; transition: transform 160ms ease; }
.langswitch[open] .langswitch__chev { transform: rotate(180deg); }

/* The masthead carries three things on a phone: the wordmark, the language,
   and the dimmer. Measured at 390px they want 402 of the 390 available, so
   one of them has to give up its words, and it is the dimmer.

   That is a ranking, not a coin toss. The dimmer is a lit dot that makes the
   page darker, and a reader who presses it sees what it did immediately and
   can press it again. The language control is the one thing on the page a
   reader may need *because they cannot read the page*, and a bare globe asks
   that reader to guess. So the language keeps a label at every width -- its
   own name where there is room, its code where there is not -- and the
   dimmer keeps only its dot, with its name still on the button for anyone
   listening to the page rather than looking at it. */
.langswitch__now { display: none; }
.langswitch__code { letter-spacing: 0.02em; }

/* Still a 44px target once it is only a dot, like everything else
   on the page that a thumb has to find. */
.dimmer { padding-inline: 0.6rem; min-width: 44px; justify-content: center; }

#dimmer-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (min-width: 34rem) {
  .langswitch__now { display: inline; }
  .langswitch__code { display: none; }

  .dimmer { padding-inline: 0.9rem; min-width: 0; justify-content: flex-start; }

  #dimmer-label {
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    clip-path: none;
    white-space: normal;
  }
}

.langswitch__panel {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 20;
  width: max-content;
  max-width: min(18rem, calc(100vw - 2rem));
  padding: 0.75rem;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 0.75rem;
  box-shadow: 0 14px 34px -14px color-mix(in srgb, var(--ink) 34%, transparent);
}

.langswitch__title {
  margin: 0 0.75rem 0.5rem;
  font-family: var(--ui);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.langswitch__list { display: grid; }

.langswitch__item {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.75rem;
  border-radius: 0.5rem;
  font-family: var(--ui);
  font-size: 0.9375rem;
  color: var(--soft);
  text-decoration: none;
}

.langswitch__item:hover { color: var(--ink); background: var(--surface2); }

/* Where the reader already is, filled rather than tinted, for the reason
   the footer list gives: it has to survive being read by someone who
   cannot separate amber from brown. */
.langswitch__item[aria-current="page"] {
  color: var(--ink);
  background: var(--surface2);
  font-weight: 600;
}

@media (prefers-reduced-motion: reduce) {
  .langswitch__chev { transition: none; }
}

/* --- The language list in the footer ---------------------------------- */

.langpick {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
}

.langpick__item {
  /* 44px tall like every other target on the page. This one is more likely
     than most to be pressed by someone who cannot read the page it is on. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.875rem;
  border: 1px solid var(--ring);
  border-radius: 999px;
  font-size: 0.8125rem;
  color: var(--muted);
}

.langpick__item:hover { color: var(--ink); border-color: var(--amber); }

/* Where the reader already is: filled, not just tinted, so it survives
   being read by someone who cannot separate amber from brown. */
.langpick__item[aria-current="page"] {
  color: var(--ink);
  background: var(--surface2);
  border-color: var(--rule);
  font-weight: 600;
}

/* --- Chinese ---------------------------------------------------------
   The three shipped faces have no Han coverage, so the zh page loads Noto
   SC alongside them (build-i18n.mjs adds it to that page's font request)
   and swaps the families here. Scoped to :lang so the other six pages are
   untouched even though they share this stylesheet.
   ------------------------------------------------------------------- */

:root:lang(zh) {
  --display: "Noto Serif SC", "Petrona", Georgia, serif;
  --body:    "Noto Serif SC", "Literata", Georgia, serif;
  --ui:      "Noto Sans SC", "Figtree", system-ui, sans-serif;
}

/* Han glyphs carry more ink per em, so the tight tracking and the balance
   the Latin display face wants both work against them. */
:lang(zh) h1,
:lang(zh) h2,
:lang(zh) h3 {
  letter-spacing: 0;
  line-height: 1.35;
  text-wrap: normal;
}

/* Named for the labels the page actually has. All four classes this rule
   used to name had been deleted from the markup, one of them by this pass
   and three long before it, so the Chinese edition was setting its small
   labels with Latin tracking and an uppercase transform that does nothing
   to Han glyphs and squeezes the Latin words mixed in among them. */
:lang(zh) .plate__meta,
:lang(zh) .book__cat,
:lang(zh) .book__free,
:lang(zh) .offer__term,
:lang(zh) .notify__label,
:lang(zh) .langswitch__title {
  letter-spacing: 0.02em;
  text-transform: none;
}


/* --- Entrance, once, and only where it earns it -------------------- */

.reveal { opacity: 0; transform: translateY(10px); }

.reveal.is-in {
  opacity: 1;
  transform: none;
  transition: opacity 600ms ease, transform 600ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; }
}

/* --- Notify form ---------------------------------------------------- */

.notify {
  margin-top: 2.75rem;
  background: var(--surface);
  border: 1px solid var(--ring);
  border-radius: 16px;
  padding: clamp(1.5rem, 4vw, 2.25rem);
  max-width: 46rem;
  box-shadow: var(--lift);
}

/* In the hero the form is the primary action rather than a card sitting in
   a section: it stands in for the button row, so it drops the card treatment
   and keeps only the field. */
.notify--hero {
  margin-top: 2.25rem;
  max-width: 32rem;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.notify--hero .notify__row { margin-top: 0; }

/* The hero's secondary link, now the only thing left in the row. */

.notify__title {
  font-family: var(--display);
  font-variation-settings: "WONK" 0;
  font-size: 1.375rem;
  font-weight: 600;
}

.notify__lede {
  margin-top: 0.5rem;
  font-size: 0.9375rem;
  color: var(--soft);
  max-width: 34rem;
}

/* The label is visible rather than a placeholder standing in for one, for
   the same reason the app's search field has one: a placeholder leaves
   exactly when the person typing might need it. */
.notify__label {
  display: block;
  flex-basis: 100%;
  font-family: var(--ui);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: -0.25rem;
}

.notify__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1.5rem;
}

.notify__input {
  flex: 1 1 16rem;
  min-height: 48px;
  padding: 0.75rem 1rem;
  font-family: var(--ui);
  font-size: 1rem;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--ring);
  border-radius: 12px;
}

.notify__input::placeholder { color: var(--muted); }

.notify__input:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 2px;
  border-color: var(--amber);
}

.notify__input[aria-invalid="true"] { border-color: var(--amber); }

.notify button[disabled] { opacity: 0.6; cursor: default; }

/* Off-screen rather than display:none: a bot reading the markup fills in
   what it finds there, while aria-hidden and tabindex keep the field away
   from anyone using a keyboard or a screen reader. */
.notify__trap {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The Turnstile widget. Its iframe sizes itself; this only spaces it from
   the row above. Empty, and so collapsed, until the widget script runs.
   With `interaction-only` (site/turnstile.mjs) the widget is 0px tall for
   an ordinary visitor but no longer `:empty`, so the margin stays: 12px
   nobody can see, kept because the checkbox lives in a closed shadow root
   and no selector can tell a hidden widget from a shown one. */
.notify__challenge { margin-top: 0.75rem; }
.notify__challenge:empty { margin-top: 0; }

/* What a visitor without JavaScript reads instead of a widget that will
   never appear. Quiet, like the status line it sits beside. */
.notify__noscript {
  margin-top: 1rem;
  font-family: var(--ui);
  font-size: 0.9375rem;
  color: var(--soft);
}

.notify__status {
  margin-top: 1rem;
  font-family: var(--ui);
  font-size: 0.9375rem;
  font-weight: 600;
  min-height: 1.5rem;
}

.notify__status:empty { margin-top: 0; min-height: 0; }

.notify__status[data-state="done"] { color: var(--sage); }
.notify__status[data-state="error"] { color: var(--amber); }
.notify__status[data-state="working"] { color: var(--muted); }

.notify__status a { color: inherit; }

/* ---------------------------------------------------------------------
   Legal pages.

   Generated by site/build-legal.mjs from docs/legal/, so this stylesheet
   is the only place their appearance is decided. These are long documents
   a worried parent actually has to read, which is a different job from the
   landing page: a wider measure than --measure, generous space between
   sections, and no text smaller than the app's own narration floor.
   --------------------------------------------------------------------- */
.legal {
  --measure-legal: 42rem;
  padding-block: clamp(2.5rem, 6vw, 4rem);
}

.legal > .wrap > * { max-width: var(--measure-legal); }

.legal h1 {
  font-size: clamp(2rem, 5vw, 2.75rem);
  margin-bottom: 1.5rem;
}

.legal h2 {
  font-size: clamp(1.375rem, 3vw, 1.75rem);
  margin-top: 3rem;
  margin-bottom: 0.875rem;
}

.legal h3 {
  font-size: 1.1875rem;
  font-weight: 600;
  margin-top: 2rem;
  margin-bottom: 0.625rem;
}

.legal h4 {
  font-family: var(--ui);
  font-size: 1rem;
  font-weight: 600;
  margin: 1.5rem 0 0.5rem;
}

.legal p { margin-bottom: 1rem; }

/* The app's narration floor is 16pt. A privacy policy is the page on a
   site people are most likely to be squinting at, so it does not get the
   smaller UI size the marketing sections use. */
.legal p,
.legal li {
  font-size: 1.0625rem;
  color: var(--soft);
}

.legal strong { color: var(--ink); font-weight: 600; }

.legal a { color: var(--amber); text-underline-offset: 3px; }
.legal a:hover { color: var(--ink); }

.legal ul {
  margin: 0 0 1.25rem;
  padding-left: 1.25rem;
}

.legal li { margin-bottom: 0.5rem; }

.legal hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 2.5rem 0;
}

.legal blockquote {
  margin: 1.5rem 0;
  padding: 1rem 1.25rem;
  border-left: 3px solid var(--honey);
  background: var(--surface2);
  border-radius: 0 10px 10px 0;
}

.legal code {
  font-family: ui-monospace, "SFMono-Regular", "Cascadia Mono", monospace;
  font-size: 0.875em;
  background: var(--surface2);
  padding: 0.1em 0.35em;
  border-radius: 4px;
}

/* Tables carry retention periods and contact routes, so they have to stay
   legible on a phone. The wrapper scrolls, never the page. */
.table-scroll {
  max-width: 100%;
  overflow-x: auto;
  margin: 0 0 1.5rem;
  border: 1px solid var(--rule);
  border-radius: 12px;
  background: var(--surface);
}

.legal .table-scroll { max-width: min(100%, 52rem); }

.table-scroll table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.9375rem;
}

.table-scroll th,
.table-scroll td {
  text-align: left;
  vertical-align: top;
  padding: 0.75rem 1rem;
  color: var(--soft);
  border-bottom: 1px solid var(--rule);
}

.table-scroll th {
  font-family: var(--ui);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

.table-scroll tr:last-child td { border-bottom: 0; }

/* An unfilled [[TOKEN]]. Shown rather than hidden: a placeholder that
   renders as blank space is a legal document quietly saying nothing where
   it should have said who we are and how to reach us. */
.legal .token {
  background: color-mix(in srgb, var(--honey) 28%, transparent);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 0.875em;
  font-weight: 600;
  padding: 0.1em 0.4em;
  border-radius: 4px;
  border: 1px dashed var(--amber);
}

/* The draft gate. Disappears on its own once the tokens are filled in,
   because the generator decides it from the source rather than a flag
   somebody has to remember to flip. */
.draft-notice {
  margin: 0 0 2.5rem;
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--amber);
  border-radius: 14px;
  background: color-mix(in srgb, var(--honey) 14%, var(--surface));
}

.draft-notice p { margin-bottom: 0.625rem; font-size: 0.9375rem; color: var(--soft); }
.draft-notice p:last-child { margin-bottom: 0; }
.draft-notice strong { color: var(--ink); }

.legal-index {
  list-style: none;
  margin: 2rem 0 2.5rem;
  padding: 0;
  display: grid;
  gap: 0.75rem;
}

.legal-index li {
  margin: 0;
  padding: 1.25rem 1.5rem;
  border: 1px solid var(--rule);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: var(--lift);
}

.legal-index a {
  font-family: var(--display);
  font-size: 1.1875rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
}

.legal-index a:hover { color: var(--amber); }

.legal-index__draft {
  font-family: var(--ui);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--amber);
  border: 1px solid var(--amber);
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  margin-left: 0.5rem;
  vertical-align: 0.1em;
}

.legal-index__blurb {
  display: block;
  margin-top: 0.35rem;
  color: var(--soft);
  font-size: 0.9375rem;
}

/* The closing form. It carries the h2 rather than sitting under one, so the
   ask and the heading are one object instead of two stacked ones. */
.notify--closing {
  margin-top: 3rem;
  max-width: none;
}

.notify--closing .notify__title { margin: 0; }

.notify--closing .notify__lede {
  margin: 0.75rem 0 0;
  color: var(--soft);
  max-width: var(--measure);
}
