/* =========================================================================
   Pigeoneer: Field Dispatch
   The system is named here. Everything below uses tokens; a raw px value in a
   component rule is a bug. design-system/ renders every one of these in
   isolation, both themes.
   ========================================================================= */

/* ---------- fonts -------------------------------------------------------
   Self-hosted, latin subset (U+0000-00FF covers English and Spanish). Only the
   four faces any rule actually selects: the Google <link> also pulled Barlow
   Condensed 500, Spectral 500 and Spectral italic, none of which are used.
   73KB, same origin, no render-blocking third-party stylesheet, and no visitor
   IP handed to a third party on a page that promises no tracking.

   The -fb faces are metric-matched fallbacks. size-adjust makes the fallback's
   advance widths equal the real font's and the overrides fix the line box, so
   the swap from fallback to webfont moves nothing. Measured off the actual
   woff2 files, not guessed. This is what takes CLS from 0.21 to 0. */

@font-face {
  font-family: "Barlow Condensed"; font-style: normal; font-weight: 700; font-display: swap;
  src: url(/assets/fonts/barlowcondensed-700-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Barlow Condensed fb"; src: local("Arial"), local("Helvetica");
  size-adjust: 71.81%; ascent-override: 139.3%; descent-override: 27.9%; line-gap-override: 0%;
}
@font-face {
  font-family: "Spectral"; font-style: normal; font-weight: 400; font-display: swap;
  src: url(/assets/fonts/spectral-400-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Spectral fb"; src: local("Georgia"), local("Times New Roman");
  size-adjust: 99.94%; ascent-override: 106.0%; descent-override: 46.3%; line-gap-override: 0%;
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap;
  src: url(/assets/fonts/ibmplexmono-400-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500; font-display: swap;
  src: url(/assets/fonts/ibmplexmono-500-latin.woff2) format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono fb"; src: local("Consolas"), local("Courier New");
  size-adjust: 109.13%; ascent-override: 93.9%; descent-override: 25.2%; line-gap-override: 0%;
}

/* @tokens:start (mirrored in design-system/tokens.css - keep byte-identical) */
:root {
  /* palette: approved and closed. Five values plus two surfaces, per theme. */
  --kraft: #E9DFC9;
  --kraft-deep: #DCCFB2;
  --paper: #F4EEDF;
  --ink: #2B2620;
  --olive: #5C5A3C;
  --signal: #C0492B;

  /* derived. Each is a blend of two palette values and nothing else; no new hue
     enters the system. They exist because the raw pair failed WCAG at the size
     it is used and the palette is not open for editing. Written as the resolved
     hex, not as color-mix(): a computed color(srgb ...) value is what the
     browser produces from color-mix, and contrast checkers misread it.
     Derivation is in the comment, ratios are measured, the DS prints both. */
  --signal-text: #933F28;   /* signal 70% + ink      5.32 kraft / 4.56 kraft-deep / 6.08 paper */
  --olive-lift:  #5C5A3C;   /* = olive               5.32 kraft / 4.56 kraft-deep / 6.08 paper */
  --ink-soft:    #514B42;   /* ink 80% + kraft       6.51 kraft / 5.59 kraft-deep / 7.45 paper */
  --on-signal:   var(--paper);   /* label colour on a signal fill, 4.29 at 19px/700 */

  /* materials. Controls cast a solid ink offset because they physically press.
     Paper objects cast a translucent one because they are sheets on a desk. */
  --offset: var(--ink);
  --shadow: color-mix(in srgb, var(--ink) 30%, transparent);
  --scrim: rgba(43, 38, 32, 0.80);

  /* type. Display is the Barlow stack only, body is Spectral prose only,
     mono is uppercase utility only. Nothing mixes across. */
  --fs-d1: clamp(48px, 7vw, 84px);   /* h1 */
  --fs-d2: clamp(30px, 4vw, 44px);   /* h2 */
  --fs-d3: 26px;                     /* wordmark */
  --fs-d4: 22px;                     /* step h3, faq summary */
  --fs-d5: 19px;                     /* button. 19 not 18: >=18.66px at 700 is
                                        WCAG large text, where 3:1 applies and
                                        paper-on-signal clears it. */
  --fs-lede: 19px;
  --fs-body: 17px;
  --fs-mono: 13px;
  --fs-mono-sm: 12px;
  --fs-mono-xs: 11px;
  --fs-mono-2xs: 10px;  /* the step number, the only label smaller than a caption */
  --fs-mono-3xs: 9px;   /* inside the dispatch band only, where a ruled cell gives it structure */
  --fs-field: 16px;     /* input and textarea. Not 17: below 16px iOS zooms the page on focus. */
  --fs-glyph: 22px;

  --ls-display-xl: 0.01em;   /* h1 only */
  --ls-display: 0.02em;      /* h2, step h3, faq summary */
  --ls-button: 0.08em;
  --ls-mono-caps: 0.12em;    /* every mono uppercase label */
  --ls-mono-wide: 0.14em;    /* marks, not labels: wordmark and stamp */
  --ls-tabular: 0.03em;      /* figures in a column, so the digits sit apart enough to scan */

  /* Optical inset for display type, measured off Barlow Condensed 700 rather than
     guessed. Its caps quantize to three left side bearings, so one blanket pull
     leaves a 1.4px spread across five headings that share a spine. Applies at
     30px and up; below that the bearing is under a pixel and the box edge wins. */
  --bear-flat: -0.03125em;   /* B C D E F G H I K L M N O P Q R U */
  --bear-diag: -0.01563em;   /* J S T V W X Y Z */
  --bear-apex: -0.008em;     /* A, which has no bearing: a small deliberate overhang */

  --lh-display: 0.95;
  --lh-body: 1.6;
  --lh-mono: 1.45;

  /* spacing: 4px grid */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px; --sp-5: 20px;
  --sp-6: 24px; --sp-8: 32px; --sp-10: 40px; --sp-12: 48px; --sp-14: 56px;
  --sp-18: 72px; --sp-24: 96px;

  --measure-lede: 46ch;
  --measure-prose: 62ch;
  --measure-answer: 68ch;
  --card-pad: var(--sp-6);
  --page: 1080px;

  /* marks. Two rotations, two meanings, no third angle.
     The substrate is never tilted: on every real dispatch artifact the stock is
     square to the frame and only the applied ink is off-axis. Tilting the paper
     and leaving the stamp near-level is the vintage-sticker tell. */
  --tilt-applied: -7deg;   /* struck by hand: the stamp */
  --tilt-cancel: -14deg;   /* the postal cancellation, struck across the page */

  /* Four rule weights and two lifts. NOTE: these are composites, so the var()
     inside them resolves where the property is DECLARED, not where it is used.
     They are therefore re-declared verbatim in the dark block below. On the site
     that is redundant because data-theme sits on :root, but the design system
     puts the theme on a descendant pane, and without the re-declaration every
     dark specimen draws the light theme's ink. */
  --rule: 2px solid var(--ink);                 /* boundary of an object you could pick up */
  --rule-hair: 1px solid color-mix(in srgb, var(--ink) 38%, transparent);  /* field division inside one object */
  --rule-hair-soft: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);  /* the second division inside one object */
  --leader: 1px dotted var(--olive);            /* a fill-in line, and only that. Dotted means
                                                   write here; solid means records divide here. */
  --leader-drop: -4px;                          /* lifts a leader onto the baseline of its value */
  /* Three states, so the button's whole press lives in tokens and can differ per
     theme. Here the press is exact: translate 4px and drop the offset by 4px, so
     the shadow's outer edge never moves and the control lands on it. */
  --lift-control: 4px 4px 0 var(--offset);
  --lift-control-hover: 3px 3px 0 var(--offset);
  --lift-control-pressed: 0 0 0 var(--offset);
  --lift-paper: 6px 6px 0 var(--shadow);

  --ease: cubic-bezier(0.2, 0, 0, 1);
}

[data-theme="dark"] {
  --kraft: #221F19;
  --kraft-deep: #2B2720;
  --paper: #2E2A22;
  --ink: #E4D6B8;
  --olive: #8C8560;
  --signal: #C45936;

  --signal-text: #D08967;   /* signal 62% + ink      5.83 kraft / 5.27 kraft-deep / 5.07 paper */
  --olive-lift:  #9F9773;   /* olive 78% + ink       5.59 kraft / 5.06 kraft-deep / 4.86 paper */
  --ink-soft:    #BDB198;   /* ink 80% + kraft       7.75 kraft / 7.01 kraft-deep / 6.74 paper */
  --on-signal:   var(--kraft);   /* 3.78 at 19px/700, against 3.28 for paper */

  /* An offset lighter than the page reads as a glow, and a glow inverts the
     material: the card looks lit from behind rather than sitting on top of the
     page. Both go darker than kraft. They are quieter than the light theme's,
     which is what a cast shadow in a dim room actually is. Depth is set to the
     light theme's own relationship, measured: there a paper shadow sits at 1.9x
     the page's linear luminance, so the dark one does too. */
  --offset: rgba(0, 0, 0, 0.50);   /* translucent, not the light theme's solid ink: a
                                      blurred opaque colour still has a slab at its core */
  --shadow: rgba(0, 0, 0, 0.55);   /* both carry more alpha because the blur dilutes them */

  /* re-declared verbatim: a composite resolves its var() at its declaration site.
     --lift-paper is the one that does not match the light theme. A hard offset is
     a light theme fact: one strong source, a sheet on a lit desk, a cut edge. The
     dark room has no such source, so everything drops a diffuse shadow, mostly
     downward with the sideways throw cut right back because a diffuse source has
     no direction to speak of. The control is tighter than the paper because it is
     a smaller object lying closer to the page. Its press is no longer the exact
     edge-meets-edge of the light theme, since a soft shadow has no edge to land
     on; it collapses to a contact shadow instead, which is what a pressed thing
     does under diffuse light. */
  --rule: 2px solid var(--ink);
  --rule-hair: 1px solid color-mix(in srgb, var(--ink) 38%, transparent);
  --rule-hair-soft: 1px solid color-mix(in srgb, var(--ink) 18%, transparent);
  --leader: 1px dotted var(--olive);
  --lift-control: 2px 5px 12px var(--offset);
  --lift-control-hover: 1px 4px 9px var(--offset);
  --lift-control-pressed: 0 1px 3px var(--offset);
  --lift-paper: 3px 8px 20px var(--shadow);
  --scrim: rgba(12, 10, 8, 0.86);
}
/* @tokens:end */

* { margin: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }

/* Paper tooth. A kraft-paper design rendered as a flat hex fill is the one place
   the direction was still only asserted: real print has grain, and every
   reference artifact in the corpus carries a single uniform texture that welds
   its unrelated shapes into one object. 3.2KB, one 160px tile, fixed and
   pointer-events:none so it costs no layout and no hit-testing. Dark runs
   lighter than light: at 5% it reads as dust rather than tooth.
   <dialog> renders in the top layer, so the lightbox is unaffected. */
/* z-index -1 puts the tooth in the BACKGROUND, under every card, plate and
   illustration, so it is the paper the page is printed on rather than a film
   laid over the ink. inset -1px covers the whole document, not the first
   viewport: body::before needs a positioned ancestor to measure against, which
   is why body is relative above. */
body::before {
  content: ""; position: absolute; inset: -1px; z-index: -1; pointer-events: none;
  background: url(/assets/grain.png) 0 0/180px repeat;
  /* Measured, not judged: at 3.5% the tooth spanned 8 of 255 levels peak to peak
     on bare kraft, which read as contrast rather than paper. 0.6% spans 1 to 2
     levels, an 80% reduction, which is just enough to break the flat fill. */
  opacity: 0.006; mix-blend-mode: multiply;
}
[data-theme="dark"] body::before { opacity: 0.007; mix-blend-mode: screen; }
body {
  background: var(--kraft);
  color: var(--ink);
  /* the tooth needs a positioned ancestor or it only covers the first viewport */
  position: relative;
  font-family: "Spectral", "Spectral fb", Georgia, serif;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}
:where(a, button, input, textarea, summary, .plate-open):focus-visible {
  outline: 3px solid var(--signal); outline-offset: 2px;
}

/* ---------- shared shapes ---------- */
.kicker, .eyebrow, .stamp, .step .no, label, .plate figcaption, .footer-inner, .dispatch dt {
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  text-transform: uppercase;
  letter-spacing: var(--ls-mono-caps);
}
h1, h2, h3, .wordmark, .btn-primary, .faq summary {
  font-family: "Barlow Condensed", "Barlow Condensed fb", sans-serif;
  font-weight: 700; text-transform: uppercase;
}

/* ---------- top bar ---------- */
/* The 2px rule stays full-bleed, but its contents land on the same 1080 column
   as every section below it. The bar used to sit 100px outboard of the H1. */
.topbar { border-bottom: var(--rule); }
.topbar-inner {
  max-width: var(--page); margin: 0 auto; padding: var(--sp-4) var(--sp-8);
  display: flex; justify-content: space-between; align-items: center;
}
.wordmark {
  font-size: var(--fs-d3); letter-spacing: var(--ls-mono-wide);
  text-decoration: none; color: var(--ink); line-height: 1;
}
.topbar nav { display: flex; align-items: center; gap: 0 var(--sp-6); }
.topbar nav a {
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace; font-size: var(--fs-mono);
  color: var(--ink); text-decoration: none; white-space: nowrap;
  border-bottom: 1px solid transparent;
  display: inline-block; padding: 13px 0;  /* ds-allow: 13 + 21.8 line + 13 = 47.8px, clears the 44px touch target */
  transition: border-bottom-color 110ms linear;
}
/* language switcher: both flags, the current one lit. SVG files, not emoji
   (Windows ships no flag glyphs and renders them as "US"/"ES" letter boxes) */
.topbar .lang { display: inline-flex; align-items: center; }
.topbar nav .lang a {
  padding: 0; border-bottom: none;
  display: inline-flex; width: 44px; height: 44px;
  align-items: center; justify-content: center;
}
.topbar .lang img {
  display: block; width: 24px; height: 16px;
  border: 1px solid var(--olive); opacity: 0.4;
  transition: opacity 110ms linear, border-color 110ms linear;
}
[data-theme="dark"] .topbar .lang img { opacity: 0.7; filter: saturate(0.6); }
.topbar .lang a[aria-current] img { opacity: 1; border-color: var(--ink); }
#theme-toggle {
  background: none; border: none; cursor: pointer;
  font-size: var(--fs-glyph); line-height: 1; color: var(--ink);
  /* fixed box: the sun and moon glyphs differ by 5px of advance width, which
     used to shove the whole nav sideways on every toggle */
  width: 44px; height: 44px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color 110ms linear;
}

/* ---------- page rhythm ---------- */
.hero, .section-inner { max-width: var(--page); margin: 0 auto; padding: var(--sp-18) var(--sp-8); }
.hero { padding-block: var(--sp-24); }
section { border-top: var(--rule); }

h1 {
  font-size: var(--fs-d1); line-height: var(--lh-display);
  letter-spacing: var(--ls-display-xl);
  /* optical alignment. Measured LSB for E is 0.03125em, which is 2.63px at 84px
     and 1.50px at 48px. The largest object on the page was the only one off the
     spine everything else shares. ES starts with D, also a flat cap. */
  margin-left: var(--bear-flat);
  margin-bottom: var(--sp-6);
}
h1 em { font-style: normal; color: var(--signal); }
h2 {
  font-size: var(--fs-d2); letter-spacing: var(--ls-display);
  line-height: var(--lh-display); margin-bottom: 0;
  /* per first letter, because one blanket pull leaves a 1.4px spread across the
     five headings the reader scrolls past on a single spine */
  margin-left: var(--bear, var(--bear-flat));
}
.shots h2 { --bear: var(--bear-diag); }        /* T */
#early-access h2 { --bear: var(--bear-apex); } /* A */
/* one rule decides the gap under every h2: 16px to prose, 32px to a component set */
h2 + * { margin-top: var(--sp-8); }
h2 + p { margin-top: var(--sp-4); }
.kicker {
  font-size: var(--fs-mono-sm); font-weight: 500; color: var(--signal-text);
  display: block; margin-bottom: var(--sp-2);
}
.section-lede { font-size: var(--fs-body); max-width: var(--measure-prose); }

/* ---------- hero ---------- */
.hero {
  display: grid; grid-template-columns: 1.2fr 0.8fr; gap: var(--sp-14);
  /* start, not center: the card's top edge now lands on the eyebrow chip's,
     which is a composition rather than a float */
  align-items: start;
}
.eyebrow {
  display: inline-block; font-size: var(--fs-mono-sm);
  color: var(--paper); background: var(--olive-lift);
  padding: 6px var(--sp-3);  /* ds-allow: optical, a 12px cap on an 8px pad reads as a slab */
  margin-bottom: var(--sp-6);
}
.lede { font-size: var(--fs-lede); max-width: var(--measure-lede); margin-bottom: var(--sp-8); }
.cta-row { display: flex; gap: var(--sp-4); align-items: center; flex-wrap: wrap; }
.btn-primary {
  display: inline-block;
  font-size: var(--fs-d5); letter-spacing: var(--ls-button);
  background: var(--signal); color: var(--on-signal);
  border: var(--rule); box-shadow: var(--lift-control);
  padding: var(--sp-4) var(--sp-8); text-decoration: none; cursor: pointer;
  transition: transform 90ms var(--ease), box-shadow 90ms var(--ease), opacity 90ms linear;
}
/* hover hints, the press bottoms it out. Gated on a real pointer so the state
   does not latch after a tap on a phone. */
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { transform: translate(1px, 1px); box-shadow: var(--lift-control-hover); }
  .topbar nav a:hover { border-bottom-color: var(--signal); }
  .topbar .lang a:hover img { opacity: 1; border-color: var(--signal); }
  #theme-toggle:hover { color: var(--signal); }
  .faq summary:hover { color: var(--signal-text); }
  .plate:hover img { border-color: var(--signal); }
}
.btn-primary:active { transform: translate(4px, 4px); box-shadow: var(--lift-control-pressed); transition-duration: 40ms; }
.btn-primary:disabled,
form[data-sending] .btn-primary {
  transform: translate(4px, 4px); box-shadow: var(--lift-control-pressed);
  opacity: 0.65; cursor: progress;
}

/* ---------- the dispatch card: the signature ---------- */
/* Squared to the frame. A Government Pigeon Service message slip is a printed
   form with a ruled field band across the head and an empty message field
   below it; only the applied ink is off-axis. */
.dispatch {
  background: var(--paper); border: var(--rule); box-shadow: var(--lift-paper);
  padding: var(--card-pad); position: relative;
}
/* A struck rubber stamp, not a UI chip. The frame is a masked SVG rather than a
   CSS border because a border can only be a perfectly sharp rectangle, and a
   rubber stamp starves at its corners and along its long runs. Same hand as
   postmark.svg, which breaks its rings the same way. The words stay live HTML,
   so both locales work from one asset and the text is still read aloud.
   It sits fully inside the card: struck on the form, not over its edge. */
.stamp {
  font-size: var(--fs-mono-xs); letter-spacing: var(--ls-mono-wide);
  color: var(--signal-text);
  display: inline-block; position: relative;
  padding: 6px 12px;  /* ds-allow: struck chip, sized to the frame's own inner rule */
  transform: rotate(var(--tilt-applied));
  margin: 0 0 var(--sp-5) var(--sp-1);
}
.stamp::before {
  content: ""; position: absolute; inset: 0;
  background: currentColor;
  -webkit-mask: url(/assets/stamp-frame.svg) 0 0 / 100% 100% no-repeat;
  mask: url(/assets/stamp-frame.svg) 0 0 / 100% 100% no-repeat;
}
.dispatch dl {
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-mono); line-height: var(--lh-mono);
  display: grid; grid-template-columns: 76px 1fr;
  margin: 0 calc(var(--card-pad) * -1);
  border-top: var(--rule-hair); border-bottom: var(--rule-hair);
}
.dispatch dt {
  font-size: var(--fs-mono-3xs); letter-spacing: var(--ls-mono-caps); color: var(--olive-lift);
  padding: 9px var(--sp-2) 9px var(--card-pad);  /* ds-allow: ruled table row, 9px reads as one cell height */
  border-right: var(--rule-hair);
}
.dispatch dd { margin: 0; padding: 9px var(--card-pad) 9px var(--sp-3); }  /* ds-allow: ruled table row, matches dt */
/* every row after the first is ruled off from the one above it */
.dispatch dt:nth-of-type(n+2), .dispatch dd:nth-of-type(n+2) { border-top: var(--rule-hair); }
/* the message field: bounded above by the band, below by the service line */
.pigeon-art { display: block; width: 100%; height: auto; margin: var(--sp-6) 0 var(--sp-5); }
.gloss {
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-mono-sm); line-height: 1.5; color: var(--olive-lift);
  margin: 0 0 var(--sp-5);
}
.dispatch-foot {
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-mono-3xs); letter-spacing: var(--ls-mono-caps); text-transform: uppercase;
  color: var(--olive-lift); text-align: center;
  border-top: var(--rule-hair);
  margin: 0 calc(var(--card-pad) * -1) calc(var(--card-pad) * -1);
  padding: 9px var(--sp-3) 10px;  /* ds-allow: ruled band foot, 9px matches the rows above it */
}

/* ---------- how it works: the standing procedure ---------- */
/* 2-up, not 3-up. At 3-up the measure was 36 characters, which is short-column
   territory carrying six paragraphs of running prose. 2-up gives 56-60, and
   six cards in 2x3 reads as a procedure rather than a feature grid. */
.steps { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-4); }
/* six copies of one printed form, so they are one height and they all lie on the
   desk. Six of the fourteen paper objects on the page used to have no lift at all,
   which in dark read as empty outlines beside lit sheets. */
.step {
  background: var(--paper); border: var(--rule); box-shadow: var(--lift-paper);
  padding: var(--card-pad); min-height: 272px;
}
.step .no {
  /* a printed field label, not a filled UI badge */
  display: block; font-size: var(--fs-mono-2xs); letter-spacing: var(--ls-mono-caps);
  color: var(--olive-lift);
  padding-bottom: 7px;  /* ds-allow: optical, the rule sits on the cap baseline */
  margin-bottom: var(--sp-3); border-bottom: var(--rule-hair);
}
.step h3 {
  font-size: var(--fs-d4); letter-spacing: var(--ls-display); line-height: 1.15;
  padding-bottom: var(--sp-2); margin-bottom: var(--sp-3);
  border-bottom: var(--rule-hair-soft);
}
.step p { font-size: var(--fs-body); }

/* ---------- the plates ---------- */
/* Detail crops at reading scale. The old 2x2 grid rendered a 1920px app at
   497 CSS px, which put its 13px body text at 3.4px: four coloured rectangles. */
.plates { display: grid; gap: var(--sp-14); }
.section-lede + .plates { margin-top: var(--sp-8); }
/* The plates sit on the page's own column like everything else. They are whole
   app windows rendered small, which is deliberate: at this size they read as
   objects in the layout rather than as documents competing with it, and the
   lightbox is where they are read, at the app's own 1920px scale.
   Below 760px there is no lightbox, so there is nothing to click through to and
   the mobile crop set takes over with something legible on its own. */
.plate { margin: 0; position: relative; }
.plate img {
  display: block; width: 100%; height: auto;
  border: var(--rule); box-shadow: var(--lift-paper);
  background: var(--kraft-deep);   /* a plate to develop on, not a hole in the layout */
  cursor: zoom-in; transition: border-color 110ms linear;
}
.plate figcaption {
  font-size: var(--fs-mono-xs); color: var(--ink-soft); padding: var(--sp-4) 0 0;
  display: flex; align-items: baseline; gap: var(--sp-3);
}
.plate figcaption .z { margin-left: auto; color: var(--signal-text); white-space: nowrap; }
@media (max-width: 759px) { .plate figcaption .z { display: none; } }
.plate figcaption .n { color: var(--signal-text); margin-right: var(--sp-3); }
.plate-open {
  display: block; padding: 0; border: 0; background: none; width: 100%; cursor: zoom-in;
}
/* the app shot runs OPPOSITE to the page theme, dark app on the light site, so
   the plate reads as a lit object either way. The class is on the <picture>,
   not the <img>: <picture> carries the desktop-vs-mobile crop axis and cannot
   also carry theme, because the site switches on [data-theme], not on
   prefers-color-scheme. */
.plate .app-light { display: none; }
[data-theme="dark"] .plate .app-dark { display: none; }
[data-theme="dark"] .plate .app-light { display: block; }
/* one rhythm: every plate is --sp-14 apart. The lead plate had a bonus 40px that
   marked nothing, since it is not otherwise distinguished from the rest. */
/* the app shot runs OPPOSITE to the page theme, dark app on the light site, so
   the plate reads as a lit object either way. The class is on the <picture>,
   not the <img>: <picture> carries the desktop-vs-mobile crop axis and cannot
   also carry theme, because the site switches on [data-theme], not on
   prefers-color-scheme. */
.plate .app-light { display: none; }
[data-theme="dark"] .plate .app-dark { display: none; }
[data-theme="dark"] .plate .app-light { display: block; }

/* ---------- lightbox ---------- */
#lightbox {
  /* margin:auto restores dialog centering killed by the global * reset */
  margin: auto; border: var(--rule); background: var(--paper);
  padding: 0; width: max-content; max-width: 96vw; max-height: 94vh;
  overflow: visible;
  opacity: 0; transform: scale(0.985);
  transition: opacity 140ms var(--ease), transform 140ms var(--ease),
              overlay 140ms allow-discrete, display 140ms allow-discrete;
}
#lightbox[open] { opacity: 1; transform: scale(1); }
@starting-style { #lightbox[open] { opacity: 0; transform: scale(0.985); } }
#lightbox::backdrop {
  background: rgba(0, 0, 0, 0);
  transition: background-color 140ms linear, overlay 140ms allow-discrete, display 140ms allow-discrete;
}
#lightbox[open]::backdrop { background: var(--scrim); }
/* The plate at 1:1, panned. The old lightbox rendered the same picture at the
   same width as the page while quietly clipping a few pixels off the tall ones,
   so enlarging read as cropping and bought nothing. This shows the identical
   crop at the app's own scale, from the 2x asset, which is always at least as
   large as the page version and on anything under a 1984px screen is a real
   magnification. It is the only place the app's 13px text reads at 13px. */
.lb-pane {
  overflow: auto; background: var(--kraft-deep);
  max-height: calc(94vh - 52px); max-width: 96vw;
  cursor: grab; overscroll-behavior: contain;
}
.lb-pane.is-dragging { cursor: grabbing; }
#lightbox img { display: block; width: 1920px; max-width: none; height: auto; }
.lb-bar {
  display: flex; align-items: center; gap: var(--sp-3);
  height: 52px; padding: 0 var(--sp-2) 0 var(--sp-4);
  border-top: var(--rule); background: var(--paper);
}
.lb-cap {
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-mono-xs); letter-spacing: var(--ls-mono-caps); text-transform: uppercase;
  color: var(--ink-soft); flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lb-cap .n { color: var(--signal-text); margin-right: var(--sp-3); }
#lightbox button {
  width: 44px; height: 44px; border: var(--rule);
  background: var(--paper); color: var(--ink);
  font: 500 var(--fs-glyph)/1 "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  cursor: pointer; flex: none;
}

/* ---------- proof: the manifest ---------- */
.proof { background: var(--kraft-deep); }
.proof-grid { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: var(--sp-12); align-items: start; }
.proof p { max-width: 58ch; margin-bottom: var(--sp-4); }
.proof-card {
  background: var(--paper); border: var(--rule); box-shadow: var(--lift-paper);
  padding: var(--card-pad);
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-mono); line-height: var(--lh-mono);
}
.proof-card ul { list-style: none; padding: 0; margin: 0; }
/* label, dotted leader, right-aligned value: every artifact that carries
   quantities sets them in a column you can scan, not inline in a sentence */
.proof-card li { display: flex; align-items: baseline; gap: var(--sp-2); padding: 9px 0; }  /* ds-allow: ruled table row */
.proof-card li > span:first-child {
  font-size: var(--fs-mono-sm); letter-spacing: var(--ls-mono-caps);
  text-transform: uppercase; color: var(--olive-lift);
}
.proof-card .lead { flex: 1; border-bottom: var(--leader); transform: translateY(var(--leader-drop)); }
.proof-card .val { font-variant-numeric: tabular-nums; letter-spacing: var(--ls-tabular); }
/* the reply rate rides with its own label so the value column stays a clean run
   of figures. Signal, because it is the one number worth pointing at. */
.proof-card li > span:first-child em {
  font-style: normal; color: var(--signal-text); letter-spacing: var(--ls-tabular);
}
.proof-card .note {
  display: block; font-size: var(--fs-mono-xs); color: var(--olive-lift);
  line-height: 1.5; padding: var(--sp-2) 0 0;
}
.proof-card .rule-total { border-top: var(--rule-hair); margin-top: var(--sp-2); padding-top: var(--sp-2); }

/* ---------- faq ---------- */
/* full-bleed ledger, measured answers. The dashed rules used to stop at 57% of
   the column and leave a 440px void that read as an oversight. */
/* The rule runs the full column, which left 561px of it spanning nothing and
   terminating in an 11px "+". The page already numbers Step 01 and Plate 01, so
   the questions are numbered too and the number hangs at the far end: the leader
   now runs from the question to its reference, which is what a leader is for. */
.faq { max-width: none; counter-reset: faq; }
.faq details { counter-increment: faq; }
.faq summary::before {
  content: "Q." counter(faq, decimal-leading-zero);
  position: absolute; right: 40px; top: 30px;  /* ds-allow: 15px pad + half a 30px first line box */
  transform: translateY(-50%);
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-mono-xs); letter-spacing: var(--ls-mono-caps);
  color: var(--olive-lift);
}
/* solid, not dotted. Dotted is a fill-in line and nothing is filled in here; the
   card interiors already use the hairline for exactly this job. */
.faq details { border-bottom: var(--rule-hair); }
.faq details:last-child { border-bottom: none; }
.faq summary {
  font-size: var(--fs-d4); letter-spacing: var(--ls-display);
  padding: 15px var(--sp-8) 21px 0;  /* ds-allow: optically centred, uppercase Barlow has empty descender space */
  cursor: pointer; position: relative; list-style: none;
  transition: color 110ms linear;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:focus-visible { outline-offset: -3px; }
.faq summary::after {
  content: "+"; position: absolute; right: 4px;
  /* anchored to the first line, not the box: four of eight summaries wrap at 320
     and the marker used to float in the gutter between lines */
  top: 30px;  /* ds-allow: anchored to the first line, not the box */
  transform: translateY(-50%); color: var(--signal-text);
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-glyph); font-weight: 400;
}
.faq details[open] summary::after { content: "\2212"; }
.faq p { max-width: var(--measure-answer); padding: 0 var(--sp-8) var(--sp-6) 0; }
:root { interpolate-size: allow-keywords; }
.faq details::details-content {
  block-size: 0; overflow: clip;
  transition: block-size 180ms var(--ease), content-visibility 180ms allow-discrete;
}
.faq details[open]::details-content { block-size: auto; }

/* ---------- signup ---------- */
/* overflow: the cancellation is struck past the section edge on purpose and is
   clipped by it, the way a real cancel runs off the paper */
#early-access { overflow: hidden; }  /* the cancel is wider than a phone */
/* Centred, with the cancellation struck large and faint behind the whole block.
   This is the composition Alfonso picked: the stamp is a watermark the form sits
   on, not an object beside it. */
.signup-inner {
  max-width: 640px; margin: 0 auto; padding: var(--sp-24) var(--sp-8);
  position: relative;
}
/* Struck in the middle of the field it composes, not sliced flat by the section's
   own 2px rule. 41% of it used to be amputated on a perfectly horizontal line,
   which reads as a misregistered print rather than a cancellation. */
.postmark {
  position: absolute; top: 50%; left: 50%;
  width: 440px; height: 440px;
  background: var(--olive); opacity: 0.05;
  -webkit-mask: url(/assets/postmark.svg) center/contain no-repeat;
  mask: url(/assets/postmark.svg) center/contain no-repeat;
  transform: translate(-50%, -52%) rotate(var(--tilt-cancel));
  pointer-events: none;
}
[data-theme="dark"] .postmark { opacity: 0.08; }
form { margin-top: var(--sp-8); display: grid; gap: var(--sp-4); }
.hint + textarea, .hint + input { margin-top: 0; }
label {
  font-size: var(--fs-mono-sm); color: var(--olive-lift);
  display: flex; align-items: baseline; gap: var(--sp-2);
  margin-bottom: 6px;  /* ds-allow: the leader sits on the label baseline */
}
/* the leader is what makes a form read as a form */
label::after { content: ""; flex: 1; border-bottom: var(--leader); transform: translateY(var(--leader-drop)); }
.hint {
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-mono-sm); line-height: 1.55; color: var(--ink-soft);
  margin: 0 0 var(--sp-2);
}
input[type="email"], textarea {
  width: 100%; padding: var(--sp-3) var(--sp-4);
  font-family: "Spectral", "Spectral fb", Georgia, serif; font-size: var(--fs-field);
  background: var(--paper); color: var(--ink);
  border: var(--rule); border-radius: 0;
}
textarea { min-height: 110px; resize: vertical; line-height: 1.5; }
input[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--signal); box-shadow: inset 0 0 0 1px var(--signal);
}
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }  /* ds-allow: offscreen honeypot */
#form-msg {
  font-family: "IBM Plex Mono", "IBM Plex Mono fb", monospace;
  font-size: var(--fs-mono); min-height: 22px; margin-top: var(--sp-1);
}
#form-msg.ok { color: var(--olive-lift); border-left: 3px solid var(--olive); padding-left: var(--sp-3); }
#form-msg.err { color: var(--signal-text); border-left: 3px solid var(--signal); padding-left: var(--sp-3); }

/* ---------- footer ---------- */
footer { border-top: var(--rule); }
.footer-inner {
  max-width: var(--page); margin: 0 auto; padding: var(--sp-8);
  font-size: var(--fs-mono-sm); color: var(--ink-soft); text-transform: none; letter-spacing: 0;
}
.footer-inner a { color: var(--ink-soft); }

/* ---------- breakpoints ---------- */
@media (max-width: 900px) {
  .proof-grid { grid-template-columns: 1fr; }

}
@media (max-width: 860px) {
  /* a real two-row bar. Inline wrap used to split "Early access" mid-phrase and
     render "access" through the letters of PIGEONEER. */
  .topbar-inner { flex-wrap: wrap; row-gap: var(--sp-1); padding: var(--sp-3) var(--sp-5); }
  .topbar nav { order: 2; width: 100%; gap: 0 var(--sp-5); flex-wrap: wrap; justify-content: flex-start; }
  .topbar .lang { margin-left: auto; }
}
/* Matched to the lightbox gate, not to a round number: above 760 you can click
   through to read the whole view at 1:1, so the page copy can be small. Below it
   there is no lightbox, so the page has to carry a legible crop on its own. */
@media (max-width: 759px) {
  .plate img { max-width: 448px; }
}
@media (max-width: 640px) {
  .hero { grid-template-columns: 1fr; padding: var(--sp-12) var(--sp-5) var(--sp-18); gap: var(--sp-10); }
  .steps { grid-template-columns: 1fr; }
  .section-inner, .signup-inner { padding: var(--sp-14) var(--sp-5); }
  .plates { gap: var(--sp-10); }
  .plate-lead { margin-bottom: var(--sp-8); }
  .postmark { width: 150px; height: 150px; right: -40px; }
}
@media (max-width: 420px) {
  .topbar nav { gap: 0 var(--sp-4); }
  .topbar nav a { font-size: var(--fs-mono-sm); }
}

/* ---------- motion policy -------------------------------------------------
   One orchestrated moment: the dispatch tag arrives, then the stamp hits.
   720ms, above the fold, then the page is still forever.

   Refused, on purpose, so the next pass does not re-propose them:
   - scroll-triggered fade-up on the step cards or section headings. Staggered
     reveal-on-scroll is the clearest AI tell there is, and these are six lines
     of a printed standing procedure that were all typed at the same moment.
   - cancelling the postmark on scroll. A page gets one stamp and it is the
     hero's; pulling the eye to decoration under a field someone is typing in
     is actively unhelpful.
   - hover lift on the dispatch, proof or step cards. None is clickable and a
     lift promises a click that does not exist.
   - any motion on the mascot. It is a printed illustration on a paper card.
   - ambient drift, parallax, a floating hero. Paper does not drift. Every
     element here is pinned to a surface by a hard offset shadow, and a
     drifting element contradicts the shadow that says it is lying on something.

   Rules held: nothing in the hero's left column ever animates (it is the LCP
   candidate); no base rule declares opacity:0, entrance states live only inside
   keyframes with fill-mode backwards, so no animation means already visible;
   no reveal is gated on JS.
   -------------------------------------------------------------------------- */
/* ds-allow: the angles below are an overshoot curve, not resting tilts. The card
   comes to rest at 0deg, which is the token value; the intermediate frames are the
   swing of a tag settling on its hole and belong to no scale. */
@keyframes tag-settle {
  0%   { transform: translateY(-10px) rotate(-2deg); }   /* ds-allow: overshoot curve */
  55%  { transform: translateY(0) rotate(1.1deg); }      /* ds-allow: overshoot curve */
  100% { transform: rotate(0deg); }                      /* ds-allow: rest */
}
/* The strike hides with visibility, never with opacity. A contrast checker that
   samples mid-animation blends a partly transparent foreground against the card
   and reports a failure that does not exist; a hidden element is simply skipped. */
@keyframes stamp-appear { from { visibility: hidden; } to { visibility: visible; } }
@keyframes stamp-hit {
  from { transform: rotate(var(--tilt-applied)) scale(1.6); }
  60%  { transform: rotate(var(--tilt-applied)) scale(0.96); }
  to   { transform: rotate(var(--tilt-applied)) scale(1); }
}
/* One event, not two. The old timing put 540ms of settle behind a 120ms lead-in
   and then struck at 520ms, by which point the card had been visually at rest for
   180ms and had spent the whole first half-second showing an empty strip where the
   stamp's layout box sat. The strike now lands while the card is still moving. */
.dispatch { transform-origin: 50% 0; animation: tag-settle 340ms cubic-bezier(0.33, 0, 0.2, 1) backwards; }
.stamp { animation: stamp-appear 1ms linear 230ms backwards, stamp-hit 200ms var(--ease) 230ms backwards; }

/* Every rule here resolves to the page exactly as it renders with motion off,
   so the reduced-motion build is a still print, which is what it should be. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .dispatch, .stamp { animation: none; }
  #lightbox, #lightbox::backdrop { transition: none; }
  .faq details::details-content { transition: none; }
  .btn-primary, .topbar nav a, .topbar .lang img, #theme-toggle, .faq summary, .plate img { transition: none; }
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}
