/*
  Keepsake marketing site stylesheet — v3 identity ("Polaroids on a table").
  Built from packages/tokens/src/{colors,spacing,radii,typography,motion}.ts —
  do not invent new colors or sizes here; add a token upstream instead.

  UNLIKE THE APP, most of the ink family here is NOT constant between light
  and dark — see docs/design/design-system.md's "v3" section and its
  "Linen & Rust palette refinement" addendum: the app wraps every real piece
  of content in a Card/Print so its ink never has to sit on the table, but
  this site has no such wrapper and puts prose straight on the table ground,
  so --ink/--ink-soft/--text2/--text3/--surface/--accent2/--accent2-text/
  --on-accent2/--rule/--foil-text genuinely flip per theme (re-hexed to the
  same kraft/walnut palette, not inverted arbitrarily). The table itself
  (--bg/--surface-sunken) and the shadow/grain respond to the theme too, for
  the obvious reason. Only the stamp red and print white
  (--accent/--accent-text/--on-accent/--accent-soft/--foil/--on-foil/--print)
  stay byte-identical in both modes — see packages/tokens/src/colors.ts's file
  header for why that specific family never inverts even here. The one
  exception is `.step-icon`, the single place this stylesheet asks stamp red
  to sit bare on the table rather than on a print; its dark-mode fix lives
  next to `.step-icon` below, not as a global override of --accent-text.
*/

/* ---------- Fonts ---------- */
/* Fraunces (variable, serif headlines/story) + CourierPrime (static, mono
   captions/labels/stamps/buttons) — served by the app at these exact paths;
   this stylesheet only references them.

   Two formats per face: the `.woff2` a browser will actually take (it stops
   at the first `src` it supports) and the `.ttf` behind it as the fallback.
   The `?v=` on every URL is a content token over apps/web/public/fonts/*,
   because those files are served `max-age=31536000, immutable` under names
   that never change — see FONT_VERSION in apps/web/app/+html.tsx for what
   goes wrong without it. The same value has to appear in both files, and
   preflight's `fonts` check recomputes it from the font bytes and fails if
   either is stale. */
@font-face {
  font-family: "Fraunces";
  src:
    url("/fonts/Fraunces-VF.woff2?v=2cd9800c") format("woff2"),
    url("/fonts/Fraunces-VF.ttf?v=2cd9800c") format("truetype-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Fraunces";
  src:
    url("/fonts/Fraunces-Italic-VF.woff2?v=2cd9800c") format("woff2"),
    url("/fonts/Fraunces-Italic-VF.ttf?v=2cd9800c") format("truetype-variations");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "CourierPrime";
  src:
    url("/fonts/CourierPrime-Regular.woff2?v=2cd9800c") format("woff2"),
    url("/fonts/CourierPrime-Regular.ttf?v=2cd9800c") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "CourierPrime";
  src:
    url("/fonts/CourierPrime-Bold.woff2?v=2cd9800c") format("woff2"),
    url("/fonts/CourierPrime-Bold.ttf?v=2cd9800c") format("truetype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "CourierPrime";
  src:
    url("/fonts/CourierPrime-Italic.woff2?v=2cd9800c") format("woff2"),
    url("/fonts/CourierPrime-Italic.ttf?v=2cd9800c") format("truetype");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "CourierPrime";
  src:
    url("/fonts/CourierPrime-BoldItalic.woff2?v=2cd9800c") format("woff2"),
    url("/fonts/CourierPrime-BoldItalic.ttf?v=2cd9800c") format("truetype");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}

/* ---------- Tokens ---------- */
:root {
  --bg: #e4ddd1;
  --surface: #faf6ee;
  --surface-sunken: #c9c0b3;
  --print: #fffdf8;
  --border: #968a7e;
  --ink: #28231f;
  --ink-soft: #49423c;
  /* Secondary copy clears 4.5:1 on both linen and paper (4.94:1 minimum). */
  --text2: #635b53;
  /* Tertiary copy remains readable where the marketing site places it on linen. */
  --text3: #635b53;
  --accent: #a9432e;
  --accent-text: #87301f;
  --on-accent: #fffdf8;
  --accent-soft: #f0ddd3;
  --accent2: #28231f;
  --accent2-text: #28231f;
  --on-accent2: #faf6ee;
  --foil: #8a6d3f;
  --foil-text: #5c4726;
  --on-foil: #fffdf8;
  --rule: rgba(40, 35, 31, 0.32);
  --shadow: rgba(40, 35, 31, 0.14);
  --shadow-strong: rgba(40, 35, 31, 0.26);
  /* The fibre tile is an OPAQUE grey image centred on 50% grey, blended into
     the ground with `overlay` (see `body::before`), not a black alpha mask
     laid over the page. Slope is the fleck's contrast; light needs less of it
     than dark because `overlay`'s response is proportional to how far the
     backdrop sits from black, and linen is much further from black than
     walnut. Tuned by eye per theme, then checked against a pixel sample. */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480'%3E%3Cfilter id='f' x='0' y='0' width='480' height='480' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.15' intercept='0.425'/%3E%3CfeFuncG type='linear' slope='0.15' intercept='0.425'/%3E%3CfeFuncB type='linear' slope='0.15' intercept='0.425'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='480' height='480' filter='url(%23f)'/%3E%3C/svg%3E");
  /* The lamp's warm pool near the top of the table (`lampLight` upstream). */
  --lamp: rgba(255, 247, 224, 0.35);

  --space-xxs: 2px;
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 12px;
  --space-lg: 16px;
  --space-xl: 24px;
  --space-xxl: 32px;
  --space-xxxl: 48px;
  --space-huge: 64px;

  /* Section rhythm is deliberately UNEQUAL, see packages/tokens/src/spacing.ts sectionRhythm. */
  --rhythm-tight: 32px;
  --rhythm-normal: 48px;
  --rhythm-loose: 96px;
  --rhythm-bookend: 112px;

  --radius-sm: 2px;
  --radius-md: 2px;
  --radius-lg: 2px;
  --radius-pill: 2px;
  --radius-tag: 2px;

  --measure-prose: 720px;
  --measure-grid: 1100px;
  --measure-hero-text: 560px;

  --duration-standard: 300ms;
  --duration-quick: 150ms;
  --duration-lift: 150ms;
  --tag-tilt: 1.4deg;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  color-scheme: light;
}

/*
  Dark mode on the marketing site: the table goes dark (walnut, --bg) and the
  running prose reads in a light ink so it stays legible directly on that
  table (the site has no literal "print" object framing every paragraph the
  way the app does — see packages/tokens/src/colors.ts's file header for why
  the app's `ink`/`pencil`/`print` are constant instead). The stamp red and
  the print white (--accent/--accent-text/--on-accent/--foil/--print) still
  never invert as custom properties — see `.step-icon` below for the one
  place that promise still needed a fix, handled as a selector override
  rather than by touching --accent-text itself.
*/
html[data-theme="dark"] {
  --bg: #2b2119;
  --surface: #3a2e23;
  --surface-sunken: #1c140f;
  --ink: #f3ecdd;
  --ink-soft: #e4d9c4;
  --text2: #cbbba0;
  --text3: #a99a82;
  /* The one "object on the table" token this site has to flip, and for
     the same reason `--text2`/`--text3` above flip: the two things that
     use it — the step numerals and the `.fleuron` — sit straight on the
     walnut table and on the dark index card, never on a print. The
     constant #5c4726 is 1.79:1 on `--bg` and 1.51:1 on `--surface` in
     dark, i.e. invisible; the numerals were legible only in light mode.
     #bb9a5c is 5.91:1 on `--bg`, 4.95:1 on `--surface` and 6.82:1 on
     `--surface-sunken`. Light keeps #5c4726 (4.77:1 on kraft). The
     Playwright axe check only scans light, so it never saw this. */
  --foil-text: #bb9a5c;
  --accent2: #e3d6be;
  --accent2-text: #e3d6be;
  --on-accent2: #2b2119;
  --rule: rgba(243, 236, 221, 0.24);
  --shadow: rgba(8, 5, 2, 0.55);
  --shadow-strong: rgba(8, 5, 2, 0.7);
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480'%3E%3Cfilter id='f' x='0' y='0' width='480' height='480' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.45' intercept='0.275'/%3E%3CfeFuncG type='linear' slope='0.45' intercept='0.275'/%3E%3CfeFuncB type='linear' slope='0.45' intercept='0.275'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='480' height='480' filter='url(%23f)'/%3E%3C/svg%3E");
  --lamp: rgba(255, 214, 140, 0.16);
  color-scheme: dark;
}

/* .step-icon is the one place stamp red sits bare on the table instead of on
   a print — the constant --accent-text (#87301f) is 1.85:1 on dark --bg and
   1.55:1 on dark --surface, invisible for a non-text 32px stroke (needs
   3:1). #d97a5c, a lighter step along the same rust hue, is 5.16:1 on --bg
   and 4.32:1 on --surface. Scoped to the selector rather than overriding
   --accent-text itself, so every other reader of that token (none today,
   but the promise stands) still gets the constant stamp red. */
html[data-theme="dark"] .step-icon {
  color: #d97a5c;
}

html[data-theme="light"] {
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  html:not([data-theme="light"]):not([data-theme="dark"]) {
    --bg: #2b2119;
    --surface: #3a2e23;
    --surface-sunken: #1c140f;
    --ink: #f3ecdd;
    --ink-soft: #e4d9c4;
    --text2: #cbbba0;
    --text3: #a99a82;
    /* The one "object on the table" token this site has to flip, and for
       the same reason `--text2`/`--text3` above flip: the two things that
       use it — the step numerals and the `.fleuron` — sit straight on the
       walnut table and on the dark index card, never on a print. The
       constant #5c4726 is 1.79:1 on `--bg` and 1.51:1 on `--surface` in
       dark, i.e. invisible; the numerals were legible only in light mode.
       #bb9a5c is 5.91:1 on `--bg`, 4.95:1 on `--surface` and 6.82:1 on
       `--surface-sunken`. Light keeps #5c4726 (4.77:1 on kraft). The
       Playwright axe check only scans light, so it never saw this. */
    --foil-text: #bb9a5c;
    --accent2: #e3d6be;
    --accent2-text: #e3d6be;
    --on-accent2: #2b2119;
    --rule: rgba(243, 236, 221, 0.24);
    --shadow: rgba(8, 5, 2, 0.55);
    --shadow-strong: rgba(8, 5, 2, 0.7);
    --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480'%3E%3Cfilter id='f' x='0' y='0' width='480' height='480' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 0 1'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.45' intercept='0.275'/%3E%3CfeFuncG type='linear' slope='0.45' intercept='0.275'/%3E%3CfeFuncB type='linear' slope='0.45' intercept='0.275'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='480' height='480' filter='url(%23f)'/%3E%3C/svg%3E");
    --lamp: rgba(255, 214, 140, 0.16);
    color-scheme: dark;
  }

  /* Mirrors the `.step-icon` override on `html[data-theme="dark"]` above —
     see that rule's comment for the measured ratios. Duplicated rather than
     shared because this whole block is the OS-preference copy of the
     explicit-theme block, kept in lockstep by
     apps/web/test/guards/sitePalette.test.ts. */
  html:not([data-theme="light"]):not([data-theme="dark"]) .step-icon {
    color: #d97a5c;
  }
}

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

html,
body {
  padding: 0;
  margin: 0;
}

html {
  overflow-x: hidden;
  /* The table's flat colour lives here, so it paints the canvas and `body`
     can stay transparent. That leaves `body::before` free to be a real ground
     layer at `z-index: -1` (below) instead of a scrim over the page. */
  background-color: var(--bg);
}

body {
  background: transparent;
  color: var(--ink);
  font-family: "CourierPrime", ui-monospace, monospace;
  font-size: 16px;
  line-height: 24px;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  position: relative;
  overflow-x: hidden;
  min-height: 100vh;
  /* Room for the consent bar, which is `position: fixed; bottom: 0` and so
     covers the end of the page while a decision is outstanding — the footer
     included, and the footer is where `[data-ks-consent]` lives. `consent.js`
     publishes its measured height here and sets it back to 0px on close; the
     fallback keeps this a no-op if that script never runs. See the
     reserving-space comment in `apps/web/public/consent.js`. */
  padding-bottom: var(--ks-consent-inset, 0px);
}

img,
svg {
  max-width: 100%;
  display: block;
}

a {
  color: inherit;
}

/* ---------- The table ----------
   The kraft/walnut ground itself: flat colour, the lamp's warm pool, a soft
   vignette, and the paper fibre — all of it BEHIND the page at `z-index: -1`,
   the same model as the app's `apps/web/src/ui/KraftTable.web.tsx`.

   This used to be a `z-index: 9999` fixed overlay carrying a black alpha mask
   (an SVG `<rect>` with no `fill`, so black) at `opacity: 1` and
   `mix-blend-mode: multiply`. That is not a texture, it is a filter over the
   whole document. Measured off a headless render, it dragged `--print` from
   #fbf9f4 to #e4e2dd, `--surface` from #f1e6cc to #ded4bc and `--bg` from
   #d2bc97 to #bda988 — a flat ~9% black veil over every card, every photo and
   every letter of type. That is the "grey layer" the founder saw on his phone.

   The fibre now lives in this layer's own background stack, and the tile is an
   OPAQUE image centred on 50% grey blended with `overlay`, rather than a black
   mask composited by alpha. That matters for more than taste: for any fixed
   backdrop `overlay` is linear in the source and 50% grey is its identity, so
   noise symmetric about 50% leaves the ground's MEAN colour exactly where the
   token says it is. Every contrast ratio quoted in `:root` therefore still
   holds with the texture switched on; an alpha mask would have darkened the
   ground under `--text2`/`--text3` and quietly broken all of them.

   Tile construction (isotropic feTurbulence, numOctaves 4, 480x480 with
   stitchTiles="stitch" so no seam shows at any width) still matches
   apps/web/public/kraft-grain.svg — only the alpha-mask ending differs. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--bg);
  background-image:
    var(--grain),
    radial-gradient(ellipse 900px 480px at 50% -60px, var(--lamp), transparent 70%),
    radial-gradient(ellipse 1400px 900px at 50% 45%, transparent 55%, rgba(0, 0, 0, 0.08) 100%);
  background-size: 480px 480px, auto, auto;
  background-repeat: repeat, no-repeat, no-repeat;
  background-blend-mode: overlay, normal, normal;
}

/* ---------- Skip link ---------- */
.skip-link {
  position: absolute;
  left: var(--space-md);
  top: -48px;
  background: var(--accent);
  color: var(--on-accent);
  padding: var(--space-sm) var(--space-lg);
  border-radius: var(--radius-sm);
  z-index: 10000;
  transition: top var(--duration-quick) var(--ease);
  font-weight: 600;
  text-decoration: none;
}

.skip-link:focus {
  top: var(--space-md);
}

/* ---------- Layout ---------- */
.wrap {
  max-width: var(--measure-grid);
  margin: 0 auto;
  padding: 0 var(--space-lg);
}

.prose {
  max-width: var(--measure-prose);
  margin: 0 auto;
  padding: 0 var(--space-lg);
}

/* ---------- Fleuron ornament (hand-drawn, not a font glyph) ---------- */
.fleuron {
  width: 28px;
  height: 28px;
  color: var(--foil-text);
  display: block;
}

.fleuron--center {
  margin: 0 auto;
}

/* ---------- Header ---------- */
.site-header {
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
  position: relative;
  z-index: 10;
}

.site-header .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  min-height: 76px;
}

.wordmark {
  font-family: "Fraunces", serif;
  font-style: italic;
  font-optical-sizing: auto;
  font-variation-settings: "SOFT" 100, "WONK" 1;
  font-weight: 600;
  font-size: 27px;
  text-decoration: none;
  color: var(--ink);
  letter-spacing: -0.01em;
}

/* The header's right-hand cluster: the plain-link nav, the Give button and
   the theme toggle, grouped so `.site-header .wrap` keeps exactly two flex
   children (wordmark, cluster) and `justify-content: space-between` still
   only has one gap to place — issue #373 finding 8. Mirrors
   `apps/web/src/ui/AppHeader.tsx`'s single row after the wordmark. */
.site-header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-xl);
}

.site-nav {
  display: flex;
  gap: var(--space-xl);
  align-items: center;
}

.site-nav a {
  text-decoration: none;
  color: var(--text2);
  font-weight: 500;
  font-size: 15px;
  padding: var(--space-sm) 0;
  border-bottom: 2px solid transparent;
  transition: color var(--duration-quick) var(--ease);
}

.site-nav a:hover {
  color: var(--ink);
}

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

/* Hidden below 640px, same as `AppHeader.tsx`'s `vp.wideOnly`: on a phone the
   header keeps only Mine, Give and the theme toggle. A `display: none` link
   drops out of the tab order and the accessibility tree on its own — no
   `aria-hidden` needed alongside it. */
@media (max-width: 640px) {
  .wide-only {
    display: none;
  }
}

/* `.btn-give`'s two labels: same trick as the app's `Give something` /
   `Give`, picked by CSS breakpoint rather than JS, because a static export
   (there isn't one here, but the principle is the same one `AppHeader.tsx`'s
   comment names) can't pick a label from the viewport for everyone. */
.btn-give-narrow {
  display: none;
}

@media (max-width: 640px) {
  .btn-give-wide {
    display: none;
  }
  .btn-give-narrow {
    display: inline;
  }
}

/* ---------- Theme toggle (issue #373 finding 8) ----------
   `/theme-toggle.js` reads and writes the same `keepsake.theme.v1` key the
   app's `ThemeProvider` does (apps/web/src/ui/theme.tsx) and cycles the same
   light -> dark -> system order as `AppHeader.tsx`'s `NEXT_MODE`. It starts
   `hidden`: with no JS there is nothing for it to do, so it stays out of the
   page entirely rather than sitting there inert — every other header control
   already works with JS off. The no-flash paint itself is the pre-hydration
   `<script>` in `<head>`, unchanged by this control. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background var(--duration-quick) var(--ease);
}

.theme-toggle[hidden] {
  display: none;
}

.theme-toggle:hover {
  background: var(--surface-sunken);
}

.theme-toggle:focus-visible {
  outline: 2px solid var(--accent2-text);
  outline-offset: 2px;
}

.theme-icon {
  display: none;
  width: 20px;
  height: 20px;
}

/* One of the three icons shows, picked by the `data-mode` the toggle's script
   sets after reading `keepsake.theme.v1` — mirrors `AppHeader.tsx`'s
   `MODE_ICON`. No `data-mode` yet (script hasn't run) reads as "system",
   the same default `ThemeProvider` assumes before hydration. */
.theme-toggle[data-mode="light"] .theme-icon-light,
.theme-toggle[data-mode="dark"] .theme-icon-dark,
.theme-toggle[data-mode="system"] .theme-icon-system,
.theme-toggle:not([data-mode]) .theme-icon-system {
  display: block;
}

/* ---------- Buttons: ONE pill (primary); secondary is a text link, not a second pill ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  min-height: 44px;
  padding: 0 var(--space-xl);
  border-radius: var(--radius-pill);
  font-family: "CourierPrime", ui-monospace, monospace;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transform: rotate(-0.8deg);
  transition:
    transform var(--duration-quick) var(--ease),
    box-shadow var(--duration-quick) var(--ease),
    background var(--duration-quick) var(--ease);
}

.btn-primary {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: 2px 3px 0px rgba(27, 24, 21, 0.3), 3px 10px 16px -8px var(--shadow-strong);
}

.btn-primary:hover {
  transform: rotate(-0.8deg) translateY(-1px);
  box-shadow: 3px 5px 0px rgba(27, 24, 21, 0.35), 5px 16px 24px -8px var(--shadow-strong);
}

.link-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  min-height: 44px;
  color: var(--ink);
  text-decoration: none;
  font-family: "CourierPrime", ui-monospace, monospace;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  border-bottom: 2px solid var(--ink);
  transition: color var(--duration-quick) var(--ease), border-color var(--duration-quick) var(--ease);
}

.link-cta:hover {
  color: var(--accent2-text);
  border-color: var(--accent2-text);
}

.link-cta .arrow {
  transition: transform var(--duration-quick) var(--ease);
}

.link-cta:hover .arrow {
  transform: translateX(3px);
}

.btn:focus-visible,
a:focus-visible,
.site-nav a:focus-visible {
  outline: 2px solid var(--accent2-text);
  outline-offset: 3px;
}

/* ---------- Hero: asymmetric 12-col grid, text on 7, seal art on 5 ---------- */
.hero {
  padding: var(--rhythm-loose) 0 var(--rhythm-normal);
}

.hero-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-xl);
  align-items: center;
}

/* ---------- Paper on the table ----------
   The hero, the why-grid and the privacy/terms prose are the three places
   where a slab of running copy would otherwise sit bare on the kraft, which
   is what made the page read as one flat tan sheet from the header to the
   footer. Each now rests on the index card `--surface` is cut from — the same
   move the app makes (see docs/design/design-system.md v3, "Layout": the
   feed's hero and filter row each sit in their own slightly-rotated Card).
   Square corners, an offset-down-and-right cast plus a 1px inset edge, so it
   reads as paper resting proud of the table rather than a rounded panel.
   Tape is reserved for prints (`.tag-card`); index cards just lie there. */
.paper,
.hero-copy,
.why-grid,
.prose-content {
  position: relative;
  background-color: var(--surface);
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, 0.08),
    2px 3px 0 var(--shadow),
    4px 14px 26px -10px var(--shadow-strong);
}

.hero-copy {
  grid-column: 1 / span 7;
  padding: var(--space-xxl) var(--space-xl) var(--space-xl);
  transform: rotate(-0.45deg);
}

.hero-art {
  grid-column: 8 / span 5;
  display: flex;
  justify-content: center;
}

.eyebrow {
  font-family: "Fraunces", serif;
  font-optical-sizing: auto;
  font-variation-settings: "opsz" 9, "SOFT" 0, "WONK" 0;
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent2-text);
  margin: 0 0 var(--space-md);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

.eyebrow .rule-tick {
  width: 22px;
  height: 1px;
  background: var(--accent2-text);
  display: inline-block;
}

h1,
h2,
h3 {
  font-family: "Fraunces", serif;
  font-optical-sizing: auto;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
}

.hero h1 {
  font-size: clamp(40px, 5.4vw, 64px);
  line-height: 0.98;
  letter-spacing: -0.02em;
  font-variation-settings: "opsz" 144, "SOFT" 30, "WONK" 1;
  margin-bottom: var(--space-lg);
}

.deck {
  font-family: "Fraunces", serif;
  font-optical-sizing: auto;
  font-variation-settings: "opsz" 9, "SOFT" 20, "WONK" 0;
  font-size: clamp(17px, 1.6vw, 19px);
  line-height: 28px;
  color: var(--text2);
  margin: 0 0 var(--space-xl);
  max-width: var(--measure-hero-text);
  font-variant-numeric: oldstyle-nums;
}

.drop-cap::first-letter {
  font-family: "Fraunces", serif;
  font-variation-settings: "opsz" 144, "SOFT" 100, "WONK" 1;
  font-weight: 600;
  font-size: 3.2em;
  line-height: 0.75;
  float: left;
  padding-right: 0.08em;
  padding-top: 0.03em;
  color: var(--ink);
}

.hero-actions {
  display: flex;
  gap: var(--space-md) var(--space-xl);
  flex-wrap: wrap;
  align-items: center;
}

/* ---------- Fade-up on load ---------- */
.fade-up {
  opacity: 0;
  transform: translateY(12px);
  animation: fadeUp var(--duration-standard) var(--ease) forwards;
}

.fade-up:nth-child(1) {
  animation-delay: 0ms;
}
.fade-up:nth-child(2) {
  animation-delay: 80ms;
}
.fade-up:nth-child(3) {
  animation-delay: 160ms;
}

@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Signature load moment: the hero's prints drop onto the table. */
.prints-drop {
  animation: printsDrop 350ms cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: 120ms;
}

@keyframes printsDrop {
  0% {
    opacity: 0;
    transform: scale(1.03) translateY(-8px);
  }
  100% {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .fade-up {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .prints-drop {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* ---------- Sections: unequal rhythm, alternating ground, torn edges between ---------- */
section {
  padding: var(--rhythm-normal) 0;
  position: relative;
}

.section-sunken {
  /* Also table, so it takes the same fibre as `body::before` — otherwise the
     ground has texture and the bands sitting on it are flat plastic. Same
     `overlay` blend, so the band's mean stays exactly `--surface-sunken`. */
  background-color: var(--surface-sunken);
  background-image: var(--grain);
  background-size: 480px 480px;
  background-blend-mode: overlay;
}

.torn-top {
  position: relative;
}

.torn-top::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 14px;
  /* Matches `.section-sunken` above explicitly rather than by `inherit`:
     `background-blend-mode` is not part of the `background` shorthand, so an
     inherited image would composite normally and leave a grey scum here. */
  background-color: var(--surface-sunken);
  background-image: var(--grain);
  background-size: 480px 480px;
  background-blend-mode: overlay;
  clip-path: polygon(
    0% 100%, 0% 40%, 4% 60%, 9% 30%, 14% 55%, 19% 25%, 24% 58%, 29% 35%, 34% 60%,
    39% 30%, 44% 55%, 49% 28%, 54% 58%, 59% 32%, 64% 60%, 69% 28%, 74% 55%,
    79% 32%, 84% 58%, 89% 30%, 94% 55%, 100% 35%, 100% 100%
  );
  transform: translateY(-100%);
}

#how {
  padding-top: var(--rhythm-tight);
}

#examples {
  padding-top: var(--rhythm-loose);
  padding-bottom: var(--rhythm-loose);
}

#why {
  padding-top: var(--rhythm-loose);
}

.closing {
  padding-top: var(--rhythm-bookend);
  padding-bottom: var(--rhythm-bookend);
}

.section-eyebrow {
  font-family: "Fraunces", serif;
  font-optical-sizing: auto;
  font-variation-settings: "opsz" 9, "SOFT" 0, "WONK" 0;
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent2-text);
  margin: 0 0 var(--space-sm);
}

h2 {
  font-size: clamp(28px, 3.2vw, 36px);
  line-height: 1.06;
  letter-spacing: -0.02em;
  font-variation-settings: "opsz" 100, "SOFT" 30, "WONK" 1;
  margin-bottom: var(--space-xl);
}

/* ---------- How it works: a dense numbered list, not a card grid ---------- */
.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}

.step {
  display: grid;
  grid-template-columns: 88px 40px 1fr;
  align-items: start;
  gap: var(--space-lg);
  padding: var(--space-xl) 0;
  border-bottom: 1px solid var(--rule);
}

.step-number {
  font-family: "Fraunces", serif;
  font-variation-settings: "opsz" 144, "SOFT" 30, "WONK" 0;
  /* Weight, not opacity, makes these recede. They used to be `--foil-text` at
     `opacity: 0.42`, which composites to 1.78:1 on the kraft table — under
     even the 3:1 large-text threshold. Full-strength foil at 4.77:1 passes at
     any size, so the numerals stay legible when the type shrinks to 32px on a
     phone. Caught by the axe check in the Playwright suite. */
  font-weight: 400;
  font-size: 56px;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--foil-text);
  margin: 0;
}

.step-icon {
  width: 32px;
  height: 32px;
  /* Constant --accent-text (#87301f) is 6.29:1 on light --bg, well clear of
     the 3:1 a non-text 32px stroke needs. Dark mode overrides this selector
     directly rather than the token — see `html[data-theme="dark"]
     .step-icon` above for the measured dark-mode value. */
  color: var(--accent-text);
  margin-top: var(--space-xs);
}

.step-text {
  padding-top: var(--space-xs);
}

.step-text p {
  margin: 0;
  font-size: 18px;
  line-height: 26px;
  color: var(--ink);
}

/* ---------- Example cards: instant-film prints, not rounded boxes ---------- */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-xxl);
  padding-top: var(--space-lg);
}

.tag-card {
  position: relative;
  background: var(--print);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08), 2px 3px 0px var(--shadow), 3px 10px 16px -8px var(--shadow-strong);
  transition: box-shadow var(--duration-standard) var(--ease), transform var(--duration-lift) var(--ease);
  transform: rotate(var(--tag-tilt)) translateY(0);
  padding: 10px;
}

.tag-card:nth-child(even) {
  transform: rotate(calc(-1 * var(--tag-tilt))) translateY(0);
}

.tag-card:hover {
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08), 3px 5px 0px var(--shadow), 5px 16px 24px -8px var(--shadow-strong);
  transform: rotate(0deg) translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
  .tag-card {
    transition: none;
  }
  .tag-card:hover {
    transform: rotate(var(--tag-tilt));
  }
}

/* A short strip of masking tape at one corner, holding the print down. */
.tag-card::before {
  content: "";
  position: absolute;
  top: -8px;
  left: 20px;
  width: 44px;
  height: 20px;
  background: rgba(205, 184, 140, 0.42);
  transform: rotate(-16deg);
  box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.12);
  z-index: 2;
}

.card-photo {
  aspect-ratio: 4 / 5;
  /* Constant, not `--surface-sunken` — see `.card-excerpt` below for why the
     print's own parts don't flip. It USED to be `--surface-sunken`, i.e.
     byte-identical to the `.section-sunken` band directly behind it, so the
     photo field, the card's surround and the table were all one colour and
     the whole section read as a single flat slab. This value is 74%
     `--surface` + 26% `--surface-sunken` at their light values: a blank sheet
     of kraft paper, plainly inside the white frame and plainly in front of
     the table. */
  background-color: #e2d3b5;
  background-image: var(--grain);
  background-size: 480px 480px;
  background-blend-mode: overlay;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl);
}

.card-photo svg {
  /* These line drawings stand in for a photograph that isn't there yet, so
     they have to read as a small pencilled hint on a lot of paper. They were
     `width/height: 100%`, which at phone width made each one ~300px tall and
     filling the entire 4:5 field — a poster, not a placeholder. Sized as a
     fraction of the field instead (proportional at every width, clamped so it
     never gets silly on a wide or narrow column), centred by the flex parent,
     and left in the shared 100x100 viewBox so the objects keep both their own
     proportions and their relative sizes: the kettle stays smaller than the
     compass, as drawn. */
  width: clamp(88px, 42%, 150px);
  height: auto;
  aspect-ratio: 1;
  flex: none;
  /* Sepia, not stamp-red: at this size a red mark competed with the one red
     thing on the page that should be shouting (the primary button), and three
     of them read as three accents. Constant, like everything else on a print.
     6.0:1 on the #e2d3b5 field. */
  color: #5c4726;
  /* Full strength at a thread's width, rather than a thick line at 70% —
     opacity on a 1px stroke just makes it mushy. */
  opacity: 1;
  stroke-width: 1.8;
}

.card-body {
  padding: var(--space-md) var(--space-xs) 0;
}

.card-excerpt {
  font-family: "Fraunces", serif;
  font-optical-sizing: auto;
  font-style: italic;
  font-variation-settings: "opsz" 9, "SOFT" 20, "WONK" 1;
  font-size: 17px;
  line-height: 24px;
  /* Constant, not `--text2`: this sits on `.tag-card`'s `--print` fill,
     which never inverts (see site.css's file header) — the caption on a
     print stays pencil-grey regardless of the room's lighting. */
  color: #635b53;
  margin: 0 0 var(--space-sm);
  position: relative;
  padding-left: 0.5em;
  text-indent: -0.5em;
}

.card-title {
  font-family: "Fraunces", serif;
  font-optical-sizing: auto;
  font-variation-settings: "opsz" 30, "SOFT" 10, "WONK" 0;
  font-weight: 600;
  font-size: 19px;
  line-height: 23px;
  margin: 0 0 var(--space-xs);
  /* Constant — see `.card-excerpt`'s comment above. */
  color: #28231f;
}

.card-caption {
  font-family: "CourierPrime", ui-monospace, monospace;
  font-size: 14px;
  line-height: 20px;
  /* Constant — see `.card-excerpt`'s comment above. */
  color: #6c6150;
  margin: 0;
}

/* ---------- Why section ---------- */
.why-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-xxl);
  padding: var(--space-xxl) var(--space-xl);
  transform: rotate(0.3deg);
}

.why-grid .fleuron {
  margin-bottom: var(--space-md);
}

.why-grid h3 {
  font-size: 21px;
  line-height: 27px;
  letter-spacing: -0.01em;
  margin-bottom: var(--space-sm);
}

.why-grid p {
  color: var(--text2);
  margin: 0;
}

/* ---------- Closing CTA ---------- */
.closing {
  text-align: center;
}

.closing-note {
  font-family: "Fraunces", serif;
  font-style: italic;
  font-variation-settings: "opsz" 9, "SOFT" 20, "WONK" 1;
  /* `--text2` is tuned for the kraft table (4.95:1) but `.closing` is a
     `.section-sunken` band, where the same colour is only 3.53:1 — the one
     pairing on this page that never cleared. `--ink-soft` is 4.81:1 on
     `--surface-sunken` light and 13.0:1 dark. */
  color: var(--ink-soft);
  font-size: 17px;
  margin: 0 0 var(--space-sm);
}

.closing h2 {
  margin-left: auto;
  margin-right: auto;
  max-width: 640px;
}

.closing .hero-actions {
  justify-content: center;
  margin-top: var(--space-xl);
}

/* ---------- Footer: a running line ---------- */
.site-footer {
  border-top: 1px solid var(--rule);
  background: var(--surface);
  padding: var(--space-xl) 0;
}

.footer-line {
  height: 1px;
  background: repeating-linear-gradient(
    to right,
    var(--rule) 0,
    var(--rule) 6px,
    transparent 6px,
    transparent 12px
  );
  margin-bottom: var(--space-lg);
}

.footer-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg) var(--space-xl);
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  color: var(--text3);
}

.footer-links {
  display: flex;
  gap: var(--space-xl);
  flex-wrap: wrap;
  align-items: center;
}

.footer-links a {
  text-decoration: none;
  color: var(--text2);
}

.footer-links a:hover {
  color: var(--ink);
}

.footer-aside {
  font-family: "Fraunces", serif;
  font-style: italic;
  font-variation-settings: "opsz" 9, "SOFT" 20, "WONK" 0;
}

/* ---------- Prose pages (privacy/terms) ---------- */
/* Same box as `.prose-content` below, so the title and the card under it share
   one left edge. The `padding` shorthand here also used to zero out `.wrap`'s
   `0 var(--space-lg)` gutter (this rule wins on source order), which put `<h1>`
   hard against the left edge of the phone screen. */
.page-header {
  box-sizing: border-box;
  width: calc(100% - 2 * var(--space-lg));
  max-width: var(--measure-prose);
  margin-inline: auto;
  padding: var(--rhythm-loose) var(--space-xl) var(--rhythm-tight);
}

.page-header h1 {
  font-size: clamp(32px, 5vw, 48px);
  letter-spacing: -0.02em;
  line-height: 1;
  font-variation-settings: "opsz" 144, "SOFT" 30, "WONK" 1;
}

.page-header .updated {
  color: var(--text3);
  font-family: "Fraunces", serif;
  font-style: italic;
  font-variation-settings: "opsz" 9, "SOFT" 20, "WONK" 0;
  font-size: 15px;
  margin-top: var(--space-md);
}

.prose-content {
  font-family: "Fraunces", serif;
  font-optical-sizing: auto;
  font-variation-settings: "opsz" 9, "SOFT" 0, "WONK" 0;
  font-size: clamp(17px, 1.6vw, 19px);
  line-height: 28px;
  color: var(--ink);
  font-variant-numeric: oldstyle-nums;
  /* Overrides `.prose`'s own box so the whole legal text is one index card on
     the table, matching `/about`'s hero and why-grid. Not rotated: it runs to
     several thousand pixels, and even a quarter of a degree throws the far
     end of that off the side of a phone. */
  box-sizing: border-box;
  width: calc(100% - 2 * var(--space-lg));
  max-width: var(--measure-prose);
  margin: 0 auto var(--rhythm-loose);
  padding: var(--space-xxl) var(--space-xl) var(--rhythm-tight);
}

.prose-content h2 {
  font-family: "CourierPrime", ui-monospace, monospace;
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent2-text);
  margin-top: var(--rhythm-tight);
  margin-bottom: var(--space-md);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--rule);
}

.prose-content p {
  margin: 0 0 var(--space-lg);
}

.prose-content ul {
  margin: 0 0 var(--space-lg);
  padding-left: var(--space-xl);
}

.prose-content li {
  margin-bottom: var(--space-sm);
}

.prose-content a {
  color: var(--accent2-text);
}

.prose-content code {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.9em;
  background: var(--surface-sunken);
  padding: 0.1em 0.4em;
  border-radius: var(--radius-sm);
}

/* ---------- Utility ---------- */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 860px) {
  .hero-grid {
    display: block;
  }
  .hero-art {
    display: none;
  }
  .step {
    grid-template-columns: 56px 32px 1fr;
  }
  .step-number {
    font-size: 40px;
  }
}

/* Under 861px the hero card is the full column width, so its rotation swings
   further; the cards give back a little side padding to keep the 40px
   headline on two lines and stay clear of the `.wrap` gutter. */
@media (max-width: 860px) {
  .hero-copy {
    padding: var(--space-xl) var(--space-lg);
    transform: rotate(-0.35deg);
  }
  .why-grid {
    padding: var(--space-xl) var(--space-lg);
    transform: rotate(0.25deg);
  }
  .prose-content {
    padding: var(--space-xl) var(--space-lg) var(--space-xxl);
  }
  .page-header {
    padding: var(--space-xxl) var(--space-lg) var(--space-xl);
  }
}

@media (max-width: 640px) {
  .site-header .wrap {
    min-height: 64px;
  }
  .site-header-actions {
    gap: var(--space-md);
  }
  .site-nav {
    gap: var(--space-lg);
  }
  .wordmark {
    font-size: 23px;
  }
  section {
    padding: var(--rhythm-tight) 0;
  }
  /* The hero card starts near the top of the phone screen — the old 48px of
     bare table above it was the first thing that read as "flat tan slab". */
  .hero {
    padding: var(--space-xxl) 0 var(--space-xxl);
  }
  #examples,
  #why {
    padding-top: var(--rhythm-normal);
    padding-bottom: var(--rhythm-normal);
  }
  .closing {
    padding-top: var(--rhythm-normal);
    padding-bottom: var(--rhythm-normal);
  }
  .step {
    grid-template-columns: 44px 28px 1fr;
    gap: var(--space-md);
  }
  .step-number {
    font-size: 32px;
  }
  /* A snapshot is a small object lying on a table, not a full-bleed panel. At
     one full-width column the print ran 358px across and its 4:5 field alone
     was 430px tall — most of a phone screen of blank paper, which is what made
     even a modest placeholder mark look like a photo that failed to load.
     Narrowed and centred, the three read as three prints laid out in a row. */
  .cards {
    grid-template-columns: min(288px, 100%);
    justify-content: center;
    gap: var(--space-xxxl);
  }
}
