/*
 * inkshift.app — one stylesheet, three pages.
 *
 * The tokens and the theme guard are lifted from src/renderer/src/styles.css
 * so the site and the app are recognisably the same object. THEME.md is the
 * brief behind both: warm, literary, generous negative space, nothing bright
 * or saturated.
 *
 * ⚠ The site does NOT carry the app's paper texture, and that is a decision
 * rather than an omission. The app is a reading surface and wants to feel like
 * one; a marketing page wants to get out of the way of the artwork it is
 * showing — thirteen photographed-paper covers and a photographed-paper
 * wordmark, all of which read better on a plain ground than on a competing
 * one. The pigment stays; the tile does not.
 *
 * ⚠ No @font-face, here or in the app. That is deliberate and not an
 * oversight: a webfont means a licence, an attribution line and a few hundred
 * KB, and the system stack renders this material better than a downloaded
 * face would.
 */

/* ─── Tokens ────────────────────────────────────────────────────────────────
 * Written THREE times, exactly as the app writes them, because the theme has
 * three states and not two. `light`/`dark` stamp data-theme; `system` removes
 * it. The media query is guarded with :not([data-theme='light']) so a visitor
 * whose OS is dark still gets dark, while an explicit choice always wins.
 *
 * The site follows the system and offers no toggle — but the guard is kept
 * intact so a toggle can be added later without rediscovering why the naive
 * version breaks.                                                            */
:root {
  --bg: #ffffff;
  --panel: #faf8f5;
  --border: #e8e4dc;
  --text: #14161a;
  --muted: #5b6472;
  --accent: #0369a1;
  --shadow: 0 1px 2px rgba(20, 22, 26, 0.04), 0 8px 24px rgba(20, 22, 26, 0.06);
  --shadow-lift: 0 2px 4px rgba(20, 22, 26, 0.06), 0 18px 48px rgba(20, 22, 26, 0.12);

  /* Site-only, and not from the app: the patch of shadow directly under a book
     on the shelf. It cannot be derived from `--shadow`, which is even on all
     four sides — that is what a card wants and what a standing object does not.
     Much heavier in the dark theme, where a 4% wash over #191817 is nothing. */
  --contact: rgba(20, 22, 26, 0.3);

  /* Site-only, and it cannot be `--bg`: the word popover sits ON the specimen
     card, so it has to be lighter than `--panel`, not lighter than the page.
     In the app the same panel is `--panel` over `--bg` and the relationship
     comes out right by itself; here the two are stacked the other way up, and
     using `--bg` painted a panel DARKER than its card in the dark theme — a
     hole punched in the page rather than a card lifted off it. */
  --pop: #ffffff;

  /* ⚠ NOT a chosen colour, and not to be nudged. #c67132 is the measured
   * median of 68,597 burnt-orange pixels in the source art — tools/make-paper.py
   * derives it rather than declaring it. Theme-invariant in the app, so
   * theme-invariant here. */
  --terracotta: #c67132;

  --wrap: 1080px;
  --nav-h: 60px;
  --radius: 12px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --bg: #191817;
    --panel: #232220;
    --border: #35322d;
    --text: #e6e8ec;
    --muted: #9aa3b2;
    --accent: #7dd3fc;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.28);
    --shadow-lift: 0 2px 4px rgba(0, 0, 0, 0.35), 0 18px 48px rgba(0, 0, 0, 0.42);
    --contact: rgba(0, 0, 0, 0.62);
    --pop: #2e2c29;
    color-scheme: dark;
  }
}

:root[data-theme='dark'] {
  --bg: #191817;
  --panel: #232220;
  --border: #35322d;
  --text: #e6e8ec;
  --muted: #9aa3b2;
  --accent: #7dd3fc;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.28);
  --shadow-lift: 0 2px 4px rgba(0, 0, 0, 0.35), 0 18px 48px rgba(0, 0, 0, 0.42);
  --contact: rgba(0, 0, 0, 0.62);
  --pop: #2e2c29;
  color-scheme: dark;
}

:root[data-theme='light'] {
  color-scheme: light;
}

/* ─── Base ──────────────────────────────────────────────────────────────── */
* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* ⚠ Every in-page link lands under the sticky bar without this. It is set on
   * the sections themselves too; this covers anything that gains an id later. */
  scroll-padding-top: calc(var(--nav-h) + 18px);
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  margin: 0;
  background-color: var(--bg);
  color: var(--text);
  font: 16px/1.6 ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}

/* Alpine sets x-cloak until it has booted; without this the mobile menu and
 * every tab panel flash open on a slow connection. */
[x-cloak] {
  display: none !important;
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

/* ⚠ `height: auto` is NOT optional, and leaving it out is not a subtle bug —
 * every screenshot renders violently stretched.
 *
 * Every <img> here carries width/height attributes, which is what stops the
 * page reflowing as images arrive. But those attributes map to *presentational
 * hints* — real CSS declarations — so `height` is 2563px, not `auto`. Pair that
 * with a width constrained to the column and the image is drawn 284px wide by
 * 2563px tall. `height: auto` releases it, and only THEN does the browser use
 * the two attributes the way they are meant to be used: as an aspect ratio. */
img {
  max-width: 100%;
  height: auto;
}

h1,
h2,
h3 {
  line-height: 1.2;
  letter-spacing: -0.02em;
  margin: 0;
  text-wrap: balance;
}

:focus-visible {
  outline: 2px solid var(--terracotta);
  outline-offset: 3px;
  border-radius: 4px;
}

.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 24px;
}

.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: 12px;
  top: 12px;
  z-index: 100;
  background: var(--panel);
  padding: 10px 16px;
  border-radius: 8px;
  border: 1px solid var(--border);
}

/* ─── Section rhythm ────────────────────────────────────────────────────── */
section {
  padding: 92px 0;
  scroll-margin-top: var(--nav-h);
}

section + section {
  border-top: 1px solid var(--border);
}

/* The small uppercase label above a section heading — the app's h2 style. */
.eyebrow {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--terracotta);
  margin: 0 0 14px;
}

.section-title {
  font-size: clamp(28px, 4vw, 38px);
  font-weight: 600;
  margin: 0 0 14px;
}

.lede {
  font-size: 18px;
  color: var(--muted);
  max-width: 62ch;
  margin: 0 0 44px;
}

/* ─── Buttons ───────────────────────────────────────────────────────────── */
/* Flat terracotta. The app prints this pigment on a paper tile (`.papered`);
 * the site does not, because the ground it would sit on is plain white and a
 * textured button on an untextured page reads as a rendering artefact rather
 * than as a material. Same colour, no tile. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 12px 22px;
  border-radius: 999px;
  border: 1px solid transparent;
  font: inherit;
  font-weight: 600;
  font-size: 15px;
  cursor: pointer;
  text-decoration: none;
  background-color: var(--terracotta);
  color: #fffcf5;
  box-shadow: var(--shadow);
}

.btn.ghost {
  background: var(--panel);
  color: var(--text);
  border-color: var(--border);
}

/* ─── Sticky bar ────────────────────────────────────────────────────────── */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--nav-h);
  /* No rule under the bar. It reads as a seam on a plain ground, and the blur
     plus the page moving beneath it is enough to say the bar is on top. */
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
}

/* Three tracks so the links sit in the TRUE centre of the bar rather than in
   the middle of whatever space the CTA leaves over. The outer two are equal
   and flexible; the menu button (narrow only) takes the first, the CTA the
   third, and either being empty costs the centring nothing. */
.nav .wrap {
  height: 100%;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 20px;
}

.nav-links {
  grid-column: 2;
  justify-self: center;
  display: flex;
  align-items: center;
  gap: 4px;
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav-links a {
  display: block;
  padding: 7px 12px;
  border-radius: 999px;
  font-size: 14.5px;
  color: var(--muted);
  text-decoration: none;
}

.nav-links a:hover {
  color: var(--text);
}

/* Set by the scroll-spy in app.js. */
.nav-links a.active {
  color: var(--text);
  background: color-mix(in srgb, var(--terracotta) 12%, transparent);
}

.nav .btn {
  grid-column: 3;
  justify-self: end;
  padding: 8px 16px;
  font-size: 14px;
}

.menu-btn {
  grid-column: 1;
  justify-self: start;
  display: none;
  background: none;
  border: 1px solid var(--border);
  border-radius: 8px;
  color: var(--text);
  padding: 7px 10px;
  cursor: pointer;
  font: inherit;
  line-height: 1;
}

/* ─── The wordmark ──────────────────────────────────────────────────────── */
/* ⚠ These four rules own `display` for the wordmark, and nothing else may set
 * it on those elements. `.hero h1 img` is (0,1,1) and `.mark-dark` is (0,1,0),
 * so a `display: block` on the container silently WINS and both cuts render at
 * once — which is exactly what shipped for a moment here, and reads as a
 * doubled logo. Container rules set size only.
 *
 * ⚠ Two images, never one recoloured by a filter (ADR 0020). The lockup is
 * photographed cut paper and its palette genuinely differs between the two
 * cuts; there is no filter that turns one into the other. Both sit in the DOM
 * and the theme hides one — the hidden one is display:none, so a screen reader
 * meets the name exactly once and identical alt text on both is safe.
 *
 * ⚠ `-light`/`-dark` names the PAGE, not the ink. logo-light.webp goes on a
 * LIGHT page. These were crossed for a period, so old code is not a reference,
 * and getting it wrong produces the one combination where the art vanishes. */
.mark-light {
  display: block;
}

.mark-dark {
  display: none;
}

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

:root[data-theme='dark'] .mark-light {
  display: none;
}
:root[data-theme='dark'] .mark-dark {
  display: block;
}

/* ─── Hero ──────────────────────────────────────────────────────────────── */
.hero {
  padding: 76px 0 84px;
  border-top: none;
}

/* ⚠ The image column is the WIDER one now, and it was 1.05fr : 0.95fr the other
   way. A fan of three needs room a single phone did not: each phone is only 44%
   of the column, so every 20px the track loses costs each of them 9px. The text
   column stays clear of `.hero-sub`'s 46ch measure at the 1080px wrap. */
.hero-grid {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  gap: 56px;
  align-items: center;
}

.hero h1 {
  margin: 0 0 26px;
}

.hero h1 img {
  width: min(340px, 78%);
  height: auto;
}

.hero-tag {
  font-size: clamp(26px, 3.6vw, 36px);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.18;
  margin: 0 0 18px;
  text-wrap: balance;
}

.hero-tag em {
  font-style: normal;
  color: var(--terracotta);
}

.hero-sub {
  font-size: 17.5px;
  color: var(--muted);
  max-width: 46ch;
  margin: 0 0 30px;
}

.cta-row {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}

/* Apple's badge artwork, used at its own aspect ratio. The guidelines require
 * the supplied asset rather than a facsimile, and a minimum height of 40px. */
.badge img {
  height: 48px;
  width: auto;
}

.badge-black {
  display: block;
}

.badge-white {
  display: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .badge-black {
    display: none;
  }
  :root:not([data-theme='light']) .badge-white {
    display: block;
  }
}

:root[data-theme='dark'] .badge-black {
  display: none;
}
:root[data-theme='dark'] .badge-white {
  display: block;
}

.cta-note {
  font-size: 13.5px;
  color: var(--muted);
  margin: 14px 0 0;
}

/* ─── Device frame ──────────────────────────────────────────────────────────
 * ⚠ The corner radius is PROPORTIONAL, not a fixed px. An iPhone 14's corners
 * are 47.33pt on a 390pt-wide screen — a ratio of 0.121 — so a radius that does
 * not scale with the frame is only ever right at one size. Fixed 40px looked
 * passable on the 300px hero and grotesque on the ~190px grid phones, where it
 * works out at 0.21 and reads as a lozenge.
 *
 * ⚠⚠ AN ELEMENT IS NOT ITS OWN QUERY CONTAINER. `container-type: inline-size`
 * used to sit on `.phone`, and `.phone`'s own `border-radius: 13.5cqw` therefore
 * did NOT resolve against the phone: with no eligible ANCESTOR container, `cqw`
 * falls back to the small viewport. Measured on an iPhone: a 126.5px-wide frame
 * with a **52.65px** radius — 13.5% of the 390px viewport, not of the phone —
 * wrapped around an image whose radius was a correct 13.6px, because the image
 * does have an ancestor container. That is what made the bezel read as an
 * uneven white border: 7px down the straight edges and a fat cream wedge in
 * every corner, growing with the viewport rather than with the phone.
 *
 * So the container is `.fan-item`, the wrapper, whose width IS the phone's
 * width — and the two radii are now concentric by construction: the bezel is
 * 6px of padding plus a 1px border, so the screen's radius is the frame's less
 * exactly 7px, at every size. */
/* No `max-width` any more: the 300px cap belonged to the single hero phone, and
   inside the fan the wrapper is what decides how big a phone is. */
.phone {
  width: 100%;
  border: 1px solid var(--border);
  background: var(--panel);
  padding: 6px;
  box-shadow: var(--shadow-lift);
  margin: 0 auto;

  /* Fallback for anything without container queries, tuned for the ~230px the
     hero draws: 12.1% of the 216px screen is 26px, plus the 7px bezel. */
  border-radius: 33px;
}

.phone img {
  display: block;
  width: 100%;
  border-radius: 26px;
}

@supports (width: 1cqw) {
  /* 12.1cqw is the screen's radius if the screen were the full width of the
     frame; it is 14px narrower, so 12.1% of that is 1.7px less. The frame then
     takes the same curve plus the 7px it stands out by. */
  .phone {
    border-radius: calc(12.1cqw + 5.3px);
  }

  .phone img {
    border-radius: calc(12.1cqw - 1.7px);
  }
}

/* ⚠ `.phone.tilt` lived here and is gone with the single hero phone it tilted.
   The fan rotates `.fan-item`, the wrapper, and not the frame itself — a
   transform on `.phone` is what the hover uses, and two rules setting one
   property means one of them does nothing. */

/* ─── The hero fan ──────────────────────────────────────────────────────────
 * Three phones of one size, the middle one in front. They are grid items in the
 * SAME cell — `grid-area: 1 / 1` — rather than absolutely positioned, so the
 * fan needs no `aspect-ratio` and no arithmetic: the items are identical, so
 * any of them sets the height, and the two leaning out from behind the middle
 * are transformed, which costs no layout at all.
 *
 * The front one is in front because of `z-index`, because the other two lean
 * away from it, and because it alone keeps `--shadow-lift`. Not because it is
 * bigger: they are all 44%.
 *
 * ⚠ It does not move. There was a scroll-driven fold here — spread at the top
 * of the page, gathering as you scrolled past — and an entrance animation
 * before that. Both are gone, deliberately: the screenshots are the argument,
 * and the biggest, stillest version of them is the one that makes it. What is
 * left is a hover lift, which nothing plays at you.
 *
 * ⚠ The spread is not a free number, and ROTATION is what costs the most. A
 * rotated phone's bounding half-width is (w·cos θ + h·sin θ) / 2, and h is
 * 2.164w — the iPhone 14's ratio — so the tall side of the phone is what eats
 * the room: at 6° a 44%-wide phone claims 26.9% of the fan either side of its
 * own centre, which leaves it 23.1% to be pushed out into. 52% of its own width
 * is that shift, and it is as wide as this fan goes at this size.
 *
 * ⚠ Nothing may hang over the edge. On a desktop the hero's gutter hides an
 * overflowing phone; at narrow widths the same overflow is a horizontal
 * scrollbar on the whole page. */
.fan {
  display: grid;
  justify-items: center;
  align-items: start;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
}

/* ⚠ The query container for the device frame, and the reason `.phone` no
   longer carries `container-type` itself — see the Device frame block above:
   an element cannot query itself, so the frame's own radius was resolving
   against the viewport. This wrapper's width IS the phone's width.

   ⚠ `width` must therefore be definite. `container-type: inline-size` applies
   inline-size containment, which stops the contents contributing to the
   element's own width — with `width: auto` the item collapses and the phone
   renders as a dot. */
.fan-item {
  grid-area: 1 / 1;
  container-type: inline-size;
  width: 44%;
}

/* The shelf sits on top. ⚠ By z-index, NOT by being written last: the DOM is
   in reading order, which is the order a screen reader takes them in. */
.fan-mid {
  z-index: 2;
}

/* ⚠ The percentages in `translate` are of the element's OWN size, not the
   fan's: 52% of a 44%-wide phone is 22.9% of the fan. Reading them as fan
   percentages is how an earlier version ended up with the sides tucked behind
   the middle. The drop is likewise a share of the phone's own height. */
.fan-left {
  transform: translate(-52%, 8%) rotate(-6deg);
}

.fan-right {
  transform: translate(52%, 8%) rotate(6deg);
}

/* ⚠ The fan sizes the frame, so the auto margins that centre a lone phone have
   to go, or the middle stops tracking the other two. */
.fan .phone {
  margin: 0;
  transition:
    transform 260ms ease,
    box-shadow 260ms ease;
}

/* The two behind carry the ordinary shadow; only the one in front is lifted.
   Three full lifts overlapping is a smudge rather than three objects — and it
   matters more now they are all one size, since the shadow is most of what
   says which one is in front. */
.fan-side .phone {
  box-shadow: var(--shadow);
}

/* Reach for one and it comes forward — the same gesture as the shelf. */
.fan-item:hover {
  z-index: 3;
}

.fan-item:hover .phone {
  transform: translateY(-10px);
  box-shadow: var(--shadow-lift);
}

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

/* ─── Woven sample ──────────────────────────────────────────────────────── */
/* A specimen of what a page looks like, set as real type rather than shown as
 * a picture, so it stays sharp and readable at any width — and carrying the
 * app's own word popover rather than a drawing of one (`.spec-pop` below).
 *
 * ⚠ The card is the size of its sentence, and the popover hangs out of it.
 * There was a measured `--reserve` on this padding for a few hours, growing the
 * card to hold a panel that is not in its layout; it left the card standing
 * half empty whenever the panel was closed, which is most of the time and every
 * time for a reader who never scrolls here. A popover overhangs its container —
 * that is what makes it a popover, and it is what the app's own does over the
 * page it is drawn on. */
.specimen {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 30px 32px;
  box-shadow: var(--shadow);
  font-size: 19px;
  line-height: 1.75;
  font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
}

/* A word standing in for one you have earned. Underlined the way the app marks
 * a tappable word, in the brand pigment.
 *
 * ⚠ `position: relative` is load-bearing, not decoration: it makes the word the
 * containing block for the popover written inside it, which is the whole reason
 * that popover needs no positioning code. */
.w {
  position: relative;
  color: var(--terracotta);
  font-weight: 600;
  border-bottom: 1.5px solid color-mix(in srgb, var(--terracotta) 40%, transparent);
}

/* ⚠ The safety net for the popover, and the reason it is on the SECTION rather
   than on the card: with no JavaScript the panel is centred under its word
   unclamped, and at a narrow width a word near the right edge would push it
   past the viewport — a horizontal scrollbar on the whole page, which is the
   one thing this layout may never produce (the fan carries the same warning).
   Clipping at the section's edge cuts only what would have scrolled. With
   JavaScript, `place()` has already moved the panel 14px inside the card, so
   this never fires. */
#how {
  overflow-x: clip;
}

/* ─── The word popover ──────────────────────────────────────────────────────
 * The panel the app shows BESIDE a word — never in place of it, so the sentence
 * you were reading does not move under your eye. Lifted from `.pop` in
 * src/renderer/src/DiglotReader.vue, down to the class names inside it, so the
 * two can be compared line by line. Two deliberate departures:
 *
 *   - the headword takes the terracotta, not `--accent`. On this page the
 *     pigment is what says "Spanish word", and `.w` above already spends it.
 *   - it always hangs below. The app flips whichever way the word sits in the
 *     viewport; here the word's position in the card is known, and a panel that
 *     hung upwards would leave the card through its top edge.
 *
 * ⚠ Never `display: none` when closed. app.js measures this box to clamp it
 * inside the card, and a box with no layout measures zero. Hidden by
 * visibility, which keeps the geometry and takes it out of the tab order. */
.spec-pop {
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  /* --dx is the clamp app.js applies when the panel would leave the card. Zero
     — dead centre under the word — is the no-JavaScript answer, and it is the
     right one everywhere the word is not near an edge. */
  transform: translateX(calc(-50% + var(--dx, 0px)));
  z-index: 5;
  width: max-content;
  max-width: min(320px, calc(100vw - 76px));
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  /* ⚠ `--pop`, not `--bg` — see the token. A panel on a card is not a panel on
     the page, and in the dark theme `--bg` is the darker of the two. */
  background: var(--pop);
  box-shadow: var(--shadow-lift);
  font-family: ui-sans-serif, -apple-system, system-ui, sans-serif;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.45;
  color: var(--text);
  text-align: left;
}

/* ⚠ Floated, not a flex row with the English — the same trap as the app's:
   putting the button in a row adds it to the box's min-width, which on a phone
   turns a long gloss into a taller, narrower panel. */
.spec-pop .say {
  display: none;
  float: right;
  margin: 0 -3px 0 8px;
  padding: 2px;
  min-width: 26px;
  min-height: 26px;
  align-items: center;
  justify-content: center;
  border: 0;
  background: none;
  color: var(--muted);
  line-height: 0;
  cursor: pointer;
}

/* Armed by app.js, because until then it has nothing to play. */
.specimen.js .spec-pop .say {
  display: inline-flex;
}

.spec-pop .say:hover {
  color: var(--terracotta);
}

.spec-pop .en {
  display: block;
  color: var(--text);
}

.spec-pop .lex {
  display: block;
  margin-top: 5px;
  font-size: 12px;
}

.spec-pop .lemma {
  color: var(--terracotta);
  font-weight: 600;
}

.spec-pop .pos {
  margin-left: 7px;
  color: var(--muted);
  font-style: italic;
}

.spec-pop .gloss {
  display: block;
  margin-top: 3px;
  color: var(--muted);
  font-size: 12px;
}

/* The headword is `ventana`; the page could as easily have said `ventanas`.
   Without this line the reader has to take the connection on faith. */
.spec-pop .forms {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: 12px;
  overflow-wrap: anywhere;
}

/* Closed is a state only a reader with JavaScript can be in: app.js stamps
   `.js` before it takes charge of the panel, so the open panel above is what
   everyone else keeps. */
.specimen.js .spec-pop {
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 0.32s ease,
    visibility 0s linear 0.32s;
}

.specimen.js.open .spec-pop {
  opacity: 1;
  visibility: visible;
  transition:
    opacity 0.32s ease,
    visibility 0s;
}

@media (prefers-reduced-motion: reduce) {
  .specimen.js .spec-pop,
  .specimen.js.open .spec-pop {
    transition: none;
  }
}

.specimen-note {
  font-size: 14px;
  color: var(--muted);
  margin: 18px 0 0;
}

/* ─── Steps ─────────────────────────────────────────────────────────────── */
.steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
  margin-top: 46px;
}

.step {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 26px 24px;
  box-shadow: var(--shadow);
}

.step-n {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 999px;
  background-color: var(--terracotta);
  color: #fffcf5;
  font-size: 14px;
  font-weight: 700;
  margin-bottom: 16px;
}

.step h3 {
  font-size: 17.5px;
  font-weight: 600;
  margin: 0 0 8px;
}

.step p {
  margin: 0;
  color: var(--muted);
  font-size: 15px;
}

/* ─── The ladder ────────────────────────────────────────────────────────── */
.rungs {
  display: grid;
  gap: 14px;
  margin-top: 40px;
}

.rung {
  display: grid;
  grid-template-columns: 128px 1fr;
  gap: 22px;
  align-items: baseline;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 22px 26px;
}

.rung-label {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.rung-body {
  font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
  font-size: 17.5px;
}

.rung-gate {
  display: block;
  font-family: ui-sans-serif, -apple-system, system-ui, sans-serif;
  font-size: 13.5px;
  color: var(--muted);
  margin-top: 6px;
}

/* ─── Figures ───────────────────────────────────────────────────────────── */
.figures {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
  margin-top: 40px;
}

.figure {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 26px 24px;
  text-align: center;
}

.figure b {
  display: block;
  font-size: 34px;
  font-weight: 600;
  letter-spacing: -0.03em;
  color: var(--terracotta);
  line-height: 1.1;
}

.figure span {
  display: block;
  font-size: 14.5px;
  color: var(--muted);
  margin-top: 8px;
}

.source-note {
  font-size: 13.5px;
  color: var(--muted);
  margin: 22px 0 0;
}

/* ─── Features ──────────────────────────────────────────────────────────── */
.features {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
}

.feature {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 26px 24px;
  box-shadow: var(--shadow);
}

.feature h3 {
  font-size: 17px;
  font-weight: 600;
  margin: 0 0 8px;
}

.feature p {
  margin: 0;
  color: var(--muted);
  font-size: 15px;
}

/* ⚠ The `.shots` grid that used to live here is gone with the markup it styled.
   The screenshots are three, fanned, in the hero — see `.fan`. */

/* ─── The shelf ─────────────────────────────────────────────────────────────
 * Covers only, and no language filter. The four editions of a book share one
 * cover — a cover describes the TEXT, not the translation (docs/03) — so tabs
 * only ever redrew the same grid, and every title carries its own name and
 * author as composited type, which makes a caption underneath a second copy of
 * something already in the picture.
 *
 * That composited type is also why the covers are drawn LARGE. This grid was
 * `auto-fill, minmax(140px, 1fr)`, which at the 1080px wrap resolves to six
 * columns of ~145px: the title on the plate is legible on the artboard and mush
 * on the page, and eight books land as a row of six with an orphaned pair
 * underneath. Four fixed tracks draw them at ~233px, in two full rows, and the
 * author's name comes back.
 *
 * The arrangement does the rest: alternating nudges and a fraction of a degree
 * of rotation, so the grid reads as shelved rather than as a table of
 * thumbnails. Small enough that no cover looks crooked on its own.             */
.shelf {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 46px 30px;
  /* Rotation and the contact shadow both push past the track; without this the
     outer covers are clipped by the section rather than tilted inside it. */
  padding: 12px 8px 34px;
}

/* ⚠ The resting tilt is a VARIABLE, not a transform, because the reveal below
   needs the same property. Two rules both setting `transform` would mean the
   last one wins and either the tilt or the animation silently does nothing;
   composing them through `--rest` keeps both.

   ⚠⚠ THE BOOKS STAND ON A LINE. Each of these used to carry a `translateY` as
   well — 0, 5, 8, 15 and 22px on a five-step cycle, co-prime with the four
   columns so that every row started one step further along it. The intent was a
   wave. Rendered, it was noise: with the offsets landing 8, 22, 0, 15 across a
   row and no shelf edge anywhere on the page for the eye to measure against,
   nothing said the raggedness was deliberate, and each book's contact shadow —
   the thing that says "resting on something" — floated at its own height. Eight
   covers is too few to establish a rhythm and more than enough to look
   misaligned.

   The tilt stays, and now it is the only thing varying. It is what carries the
   hand-placed feeling on its own: `transform-origin: 50% 100%` turns each book
   about its foot, so a lean costs the baseline nothing. Keep any new value in
   this cycle a bare `rotate()` under a degree. */
.book:nth-child(5n + 1) {
  --rest: rotate(-0.7deg);
}
.book:nth-child(5n + 2) {
  --rest: rotate(0.5deg);
}
.book:nth-child(5n + 3) {
  --rest: rotate(-0.4deg);
}
.book:nth-child(5n + 4) {
  --rest: rotate(0.8deg);
}
.book:nth-child(5n) {
  --rest: rotate(-0.2deg);
}

/* The wrapper exists so the cover can have a spine and a shadow on the ground:
   an <img> has no pseudo-elements to hang either on. It carries the tilt and
   the reveal; the art inside it carries the paper. */
.book {
  position: relative;
  /* Tilt about the foot, the way a book leans on a shelf, rather than about
     the middle, which swings the top and bottom in opposite directions. */
  transform-origin: 50% 100%;
  /* ⚠ Load-bearing: the contact shadow below is `z-index: -1`, and without a
     stacking context here it escapes to the root and paints under the page
     rather than under the book. `opacity` and `transform` happen to make one in
     the animated states, so the bug would appear only under reduced motion —
     the path least likely to be looked at. */
  isolation: isolate;

  /* Before it is seen: lifted slightly off the shelf and out of focus. */
  opacity: 0;
  transform: translateY(28px) scale(0.97);
  transition:
    opacity 620ms cubic-bezier(0.2, 0.7, 0.3, 1),
    transform 620ms cubic-bezier(0.2, 0.7, 0.3, 1);
}

.book-art {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  /* Tight on the spine edge, rounded on the fore-edge — the asymmetry is what
     tells you which side the binding is on. */
  border-radius: 2px 6px 6px 2px;
  background: var(--panel);
  box-shadow: var(--shadow);
  transition: box-shadow 220ms ease;
}

/* The spine: a dark gutter, a highlight where the board turns, and a short
   falloff across the front. Overlaid on the artwork rather than beside it, so
   it costs no layout and works on every cover regardless of what is painted
   there. ⚠ pointer-events: none, or it eats the hover it is decorating. */
.book::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: 2px 6px 6px 2px;
  background: linear-gradient(
    90deg,
    rgba(20, 22, 26, 0.34) 0,
    rgba(20, 22, 26, 0.1) 1.6%,
    rgba(255, 255, 255, 0.2) 3.2%,
    rgba(20, 22, 26, 0.1) 5%,
    rgba(20, 22, 26, 0.03) 9%,
    rgba(20, 22, 26, 0) 15%
  );
}

/* Contact with the shelf. `--shadow` is a card shadow — even on all four sides,
   which reads as floating; this is the bit directly underneath. It sits inside
   the tilted wrapper, so it leans with the book. */
.book::after {
  content: '';
  position: absolute;
  left: 5%;
  right: 5%;
  bottom: -13px;
  height: 18px;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(50% 50% at 50% 50%, var(--contact), transparent 70%);
  transition: opacity 220ms ease;
}

/* Set by the observer in app.js as each cover enters the viewport. The delay
   is a small per-column stagger so a row lands like a hand setting books down,
   left to right, rather than all at once. */
.book.in {
  opacity: 1;
  transform: var(--rest);
  transition-delay: calc(var(--i, 0) * 70ms);
}

/* Straightens and lifts — the one thing a shelf of paper objects should do
   when you reach for one. ⚠ After `.in` so it wins once revealed. */
.book.in:hover {
  transform: rotate(0deg) translateY(-10px) scale(1.03);
  transition-delay: 0ms;
}

.book.in:hover .book-art {
  box-shadow: var(--shadow-lift);
}

/* Lifted off the shelf, so the contact patch spreads and fades. */
.book.in:hover::after {
  opacity: 0.45;
}

/* ⚠ Motion is an enhancement here and the covers are the content, so the
   reduced-motion path must leave them VISIBLE rather than merely still —
   `transform: none` alone would keep opacity 0 and hide the whole shelf.
   app.js also reveals them immediately rather than waiting on the observer. */
@media (prefers-reduced-motion: reduce) {
  .book,
  .book.in,
  .book.in:hover {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ─── FAQ ───────────────────────────────────────────────────────────────── */
.faq {
  max-width: 780px;
  border-top: 1px solid var(--border);
}

.faq-item {
  border-bottom: 1px solid var(--border);
}

.faq-q {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 18px;
  padding: 20px 2px;
  background: none;
  border: none;
  font: inherit;
  font-size: 16.5px;
  font-weight: 600;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.faq-q span {
  color: var(--terracotta);
  font-size: 20px;
  flex: 0 0 auto;
  line-height: 1;
}

.faq-a {
  padding: 0 2px 22px;
  color: var(--muted);
  max-width: 68ch;
}

.faq-a p {
  margin: 0 0 12px;
}

.faq-a p:last-child {
  margin-bottom: 0;
}

/* ─── Contact ───────────────────────────────────────────────────────────── */
.contact {
  margin-top: 44px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 30px 32px;
  box-shadow: var(--shadow);
  max-width: 780px;
}

.contact h3 {
  font-size: 18px;
  font-weight: 600;
  margin: 0 0 10px;
}

.contact p {
  color: var(--muted);
  margin: 0 0 16px;
  max-width: 60ch;
}

.contact ul {
  color: var(--muted);
  margin: 0 0 20px;
  padding-left: 20px;
}

.contact li {
  margin-bottom: 5px;
}

/* ─── Footer ────────────────────────────────────────────────────────────── */
/* ─── Footer ────────────────────────────────────────────────────────────────
 * Inverted, in both themes: a dark slab that closes the page.
 *
 * It redefines the TOKENS rather than setting a background and a colour,
 * because everything inside it — the rule above the credits, the link colour,
 * the muted body text — is drawn from those tokens. Overriding two properties
 * would have left a light-theme border and a dark-theme-only link colour
 * inside a dark box. This way the whole subtree simply is dark.
 *
 * ⚠ In dark mode the page ground is #191817 and this is #14161a, which is
 * close but not equal — the `border-top` is what actually separates them, and
 * it is load-bearing rather than decorative. */
footer {
  --bg: #14161a;
  --panel: #1f1e1c;
  --border: #35322d;
  --text: #f4efe6;
  --muted: #a9a297;
  --accent: #7dd3fc;
  color-scheme: dark;

  background: var(--bg);
  border-top: 1px solid var(--border);
  padding: 54px 0 64px;
  font-size: 14px;
  color: var(--muted);
}

/* ⚠ The footer is dark whatever the page is, so it always takes the DARK-page
   cut of the wordmark. Without this the light cut renders on a dark slab in
   light mode — the exact combination ADR 0020 warns makes the art vanish, and
   the theme rules above cannot know about a locally inverted block. */
footer .mark-light {
  display: none;
}

footer .mark-dark {
  display: block;
}

.foot-grid {
  display: flex;
  justify-content: space-between;
  gap: 40px;
  flex-wrap: wrap;
  margin-bottom: 34px;
}

.foot-links {
  display: flex;
  gap: 22px;
  flex-wrap: wrap;
}

.foot-mark img {
  height: 20px;
  width: auto;
  margin-bottom: 12px;
}

.credits {
  border-top: 1px solid var(--border);
  padding-top: 26px;
  font-size: 13px;
  max-width: 78ch;
}

.credits p {
  margin: 0 0 9px;
}

/* ─── Legal pages ───────────────────────────────────────────────────────── */
.prose {
  max-width: 720px;
  margin: 0 auto;
  padding: 60px 24px 96px;
}

.prose h1 {
  font-size: clamp(30px, 5vw, 40px);
  font-weight: 600;
  margin: 0 0 12px;
}

.prose .updated {
  color: var(--muted);
  font-size: 14px;
  margin: 0 0 40px;
}

.prose h2 {
  font-size: 21px;
  font-weight: 600;
  margin: 46px 0 14px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
}

.prose h3 {
  font-size: 16.5px;
  font-weight: 600;
  margin: 28px 0 10px;
}

.prose p,
.prose li {
  color: var(--text);
}

.prose p {
  margin: 0 0 16px;
}

.prose ul,
.prose ol {
  margin: 0 0 16px;
  padding-left: 22px;
}

.prose li {
  margin-bottom: 8px;
}

.prose code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.88em;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 5px;
}

/* The plain-English summary at the top of the privacy policy AND the terms —
 * one rule, both pages. It is the part most people will actually read, so it
 * is set as the loudest thing on the page rather than as a preamble to the
 * real document.
 *
 * The panel and the shadow do that work on their own. It carried a 3px
 * terracotta left border until 2026-08-16; it read as a pull-quote or an
 * editor's flag — a marker on the document rather than part of it — which is
 * the opposite of what these two boxes are for. */
.summary {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 26px 30px;
  margin: 0 0 40px;
  box-shadow: var(--shadow);
}

.summary h2 {
  border: none;
  padding: 0;
  margin: 0 0 12px;
  font-size: 18px;
}

.summary ul {
  margin: 0;
  padding-left: 20px;
}

.summary li {
  margin-bottom: 7px;
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 20px;
  font-size: 14.5px;
}

.prose th,
.prose td {
  text-align: left;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.prose th {
  color: var(--muted);
  font-weight: 600;
  font-size: 12.5px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

.back {
  display: inline-block;
  margin-bottom: 28px;
  font-size: 14.5px;
  text-decoration: none;
}

/* ─── Narrow ────────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  .hero-grid {
    grid-template-columns: 1fr;
    gap: 44px;
  }

  .hero h1 img {
    width: min(300px, 70%);
  }

  .steps,
  .features,
  .figures {
    grid-template-columns: 1fr;
  }

  .rung {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  /* ⚠ The column count is explicit at every width, not auto-filled, because
     the covers must stay big enough to read the title composited into them.
     Three across down to 560px, then two — never one, which draws a 300px-wide
     book on a phone and turns the shelf into a slideshow. */
  .shelf {
    grid-template-columns: repeat(3, 1fr);
    gap: 38px 26px;
  }
}

@media (max-width: 560px) {
  .shelf {
    grid-template-columns: repeat(2, 1fr);
    gap: 32px 22px;
  }
}

@media (max-width: 760px) {
  section {
    padding: 68px 0;
  }

  .menu-btn {
    display: block;
  }

  /* The links leave the bar and become a sheet under it. `display: none` is
   * what closes them — not opacity or height — so a closed menu is genuinely
   * out of the tab order and a keyboard user never lands in an invisible list. */
  .nav-links {
    display: none;
    position: absolute;
    top: var(--nav-h);
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: 12px;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-lift);
  }

  .nav-links.open {
    display: flex;
  }

  .nav-links a {
    padding: 11px 14px;
    font-size: 16px;
  }

  .nav .btn {
    display: none;
  }

  .specimen {
    padding: 24px 22px;
    font-size: 17.5px;
  }

  .contact,
  .summary {
    padding: 24px 22px;
  }
}
