/*
 * The site's whole stylesheet. Values come from design/tokens.json, not from
 * taste - the page has to sit next to the app without arguing with it.
 *
 * Deliberately zero third-party requests: the face, the texture and the type
 * are all served from this origin. A privacy page that phones a CDN home to
 * render itself is a page that contradicts its own text.
 */

@font-face {
  font-family: 'Courier Prime';
  src: url('fonts/CourierPrime-Regular.ttf') format('truetype');
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: 'Courier Prime';
  src: url('fonts/CourierPrime-Bold.ttf') format('truetype');
  font-weight: 700;
  font-display: swap;
}

:root {
  --paper: #e9e1d1;
  --surface: #f1ebdd;
  --ink: #1e1b16;
  --ink-muted: #6a645a;
  --divider: #d8cebc;
  --accent: #b58e7a;
  --accent-edge: #a9836e;
  --accent-text: #8a5240;

  --measure: 34rem;
}

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

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

body {
  margin: 0;
  padding: 0;
  color: var(--ink);
  /* No background here: the textured canvas above shows through. */
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 17px;
  line-height: 1.6;
}

/*
 * Two procedural noise layers over the flat fill, the same pair the app
 * composites: grain for the fibre, mottle for the staining. Both are inline
 * SVG so they stay sharp at any density, and both are fixed so a long page
 * does not visibly slide its own paper as it scrolls.
 *
 * Tile, frequency, octaves and seed are paper.grain and paper.mottle in
 * design/tokens.json. The amplitudes are lower than the app's: the app shows
 * the texture behind three shapes, this shows it behind a thousand words, and
 * the app's 0.32 of grain under 17px body text reads as a dirty screen.
 *
 * Layers on the canvas background, NOT a blended pseudo-element. The obvious
 * version - a fixed body::before at z-index -1 with mix-blend-mode: multiply -
 * renders as nothing at all: body creates no stacking context, so a negative
 * z-index child paints behind the canvas background rather than on top of it,
 * and the whole texture disappears under the flat fill. It survives review
 * because the page still looks correct; only measuring the pixels catches it
 * (the crop came back one single value, 0 variance). Do not "tidy" this back
 * into a pseudo-element.
 */
html {
  background-color: var(--paper);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='620' height='620'%3E%3Cfilter id='m'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.038' numOctaves='3' seed='21'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='620' height='620' filter='url(%23m)' opacity='0.09'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' seed='7'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)' opacity='0.2'/%3E%3C/svg%3E");
  background-size: 620px 620px, 220px 220px;
  background-attachment: fixed, fixed;
}

/* --- shell ------------------------------------------------------------- */

.page {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 4rem 1.5rem 5rem;
}

/*
 * The mark, as the app's tile rather than as a flat square.
 *
 * components/Tile.tsx calls this "the app's one object", and it is not just the
 * face: it is the face on a raised square of paper. Drawing it flat here made
 * the page and the app disagree about the one thing they share. Everything below
 * is the same token the component reads.
 *
 * `src` is face.svg, not mark.svg. An img shows its own background wherever its
 * source is transparent, so the fill, the texture and the gradient sit behind
 * the three shapes with no wrapper element. mark.svg's opaque rect would cover
 * the lot.
 *
 * ⚠️ `--k` scales the shadow with the mark. The elevation tokens are stated for
 * a 208pt tile, and dropping a 208pt shadow under a 72px mark reads as a much
 * heavier object than the app's. One definition, scaled, rather than a second
 * set of numbers to keep in step.
 *
 * The corner is `border-radius`, which is a circular arc - the app gets a
 * continuous curve from `borderCurve` and CSS has no equivalent. The difference
 * is small at this size and still small at wallpaper size.
 *
 * ⚠️ This rule is the wallpaper's tile. design/bake-wallpaper.py renders both
 * wallpapers from this stylesheet in headless Chrome, so a change here changes
 * them at their next bake.
 */
.mark {
  --size: 72px;
  --k: 0.346; /* 72 / 208 */

  display: block;
  width: var(--size);
  height: var(--size);
  margin-bottom: 2.5rem;
  margin-inline: auto;

  border-radius: 23%;
  border: 1px solid rgba(171, 158, 136, 0.5);
  background-color: var(--surface);

  /* The same grain and mottle the canvas runs. Not `fixed` like the canvas, and
     offset on top of that: aligned fibres make the tile read as a hole cut in
     the paper rather than a second sheet resting on it (paper.offset).

     No face gradient. It was removed everywhere the key is drawn - see
     elevation.key.$faceRemovedComment in design/tokens.json. The thickness is
     carried by the border and the elevation.key shadow below, at the edges,
     which is where paper shows it. */
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='620' height='620'%3E%3Cfilter id='m'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.038' numOctaves='3' seed='21'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='620' height='620' filter='url(%23m)' opacity='0.09'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' seed='7'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23g)' opacity='0.2'/%3E%3C/svg%3E");
  background-size: 620px 620px, 220px 220px;
  background-position:
    40px 462px,
    85px 140px;

  /* elevation.key: four outer layers so the falloff is continuous rather than
     stepped, plus the two inset lips that give it thickness. */
  box-shadow:
    0 calc(1px * var(--k)) calc(2px * var(--k)) rgba(74, 58, 42, 0.11),
    0 calc(4px * var(--k)) calc(8px * var(--k)) rgba(74, 58, 42, 0.1),
    0 calc(12px * var(--k)) calc(22px * var(--k)) rgba(74, 58, 42, 0.11),
    0 calc(26px * var(--k)) calc(46px * var(--k)) rgba(74, 58, 42, 0.1),
    inset 0 calc(1.5px * var(--k)) 0 rgba(255, 253, 248, 0.92),
    inset 0 calc(-2.5px * var(--k)) 0 rgba(74, 58, 42, 0.085);
}

/* --- type -------------------------------------------------------------- */

h1,
h2,
.wordmark {
  font-family: 'Courier Prime', ui-monospace, 'Courier New', monospace;
  font-weight: 400;
  letter-spacing: -0.01em;
}

h1 {
  font-size: 2rem;
  line-height: 1.2;
  margin: 0 0 0.5rem;
  text-align: center;
}

/* One line under the title: what this is, and when it started saying so.
   Centred with the mark and the title as one header block; the prose under
   it stays left, because these pages are read rather than glanced at. */
.stamp {
  color: var(--ink-muted);
  font-size: 0.9375rem;
  margin: 0 0 3rem;
  text-align: center;
}

h2 {
  font-size: 1.1875rem;
  margin: 3rem 0 0.75rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--divider);
}

/* The first rule under the stamp would double the stamp's own gap. */
h2:first-of-type {
  margin-top: 0;
}

p {
  margin: 0 0 1rem;
}

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

li {
  margin-bottom: 0.375rem;
}

/* The lead paragraph carries the summary, so it gets the weight the
   headings do not. */
.lead {
  font-size: 1.125rem;
}

strong {
  font-weight: 600;
}

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

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

/* --- blocks ------------------------------------------------------------ */

/*
 * A key: the tile the app uses for anything sitting ON the paper rather than
 * IN it. Same surface colour, same continuous-corner feel.
 */
.key {
  background: var(--surface);
  border: 1px solid var(--divider);
  border-radius: 14px;
  padding: 1.25rem 1.375rem;
  margin: 0 0 1rem;
}

.key p:last-child,
.key ul:last-child {
  margin-bottom: 0;
}

/* Things the app does not do. A list of absences, set quieter than a list of
   facts, because that is what it is. */
.none li {
  color: var(--ink-muted);
}

footer {
  margin-top: 4rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--divider);
  color: var(--ink-muted);
  font-size: 0.9375rem;
}

/* Flex with a gap rather than a margin on each link: a trailing margin on the
   last one pulls the footer off centre wherever it is centred.

   ⚠️ The row gap is 0, not 0.5rem, and that is not a tightening. The links
   below carry 0.625rem of vertical padding to reach the minimum tap target;
   that padding is invisible but it is still space, so leaving a row gap on
   top of it would separate two wrapped rows by 28px of nothing. 0 puts the
   tap targets edge to edge and leaves the text 20px apart, which is close to
   what the 0.5rem gap gave before the padding existed.

   The negative block margin takes back the padding the first and last rows
   add, so the footer's height, and its distance from the rule above it, are
   what they were. */
footer nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.25rem;
  margin-block: -0.625rem;
}

/* 24px of line box + 2 × 10px = 44px, which is WCAG 2.5.5 and the same 44pt
   minimum the app is built to. They were 24px tall, which is a target you
   miss on a phone.

   Vertical only: the 1.25rem column gap already separates them horizontally,
   and padding-inline would widen every link and make the row wrap sooner. */
footer nav a {
  padding-block: 0.625rem;
}

/* --- landing ----------------------------------------------------------- */

/*
 * index.html's hero only. It began as the whole page - a holding page, which
 * said the true thing and stopped - and it still looks like one; what changed
 * is that there is now prose underneath it, so this is a section rather than
 * the document.
 *
 * ⚠️ 88svh, not 100. A hero that fills the viewport exactly is a hero that
 * hides everything after it: centred content with nothing overlapping the
 * bottom edge reads as the end of the page, and the section below it never
 * gets scrolled to. 88 leaves the next heading just cut off, which is the
 * whole of the affordance and costs no chevron, no script and no arrow.
 *
 * svh rather than vh so the mobile toolbar collapsing does not resize it
 * mid-scroll; the vh line above is the fallback for anything that predates
 * svh, and has to stay first.
 */
.holding {
  min-height: 88vh;
  min-height: 88svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: center;
  align-items: center;
}

.holding .mark {
  --size: 128px;
  --k: 0.615; /* 128 / 208 */

  margin-bottom: 2rem;
}

/* The wordmark is an <h1> — the holding page is the site's most important URL
   and the heading is real, not decorative. It took its colour from `.holding p`
   and its line-height from `body` while it was a <p>; as an h1 it matches
   neither, so both are restated here to keep the page pixel-identical. */
/* `.stamp` carries a 3rem bottom margin, which is right on the document pages
   where a header block has to separate itself from a thousand words of prose.
   Here it is one short line above four more, and 3rem read as a gap rather
   than a separation. 2rem matches the space the mark and the store link
   already use, so the page keeps one rhythm: 32 / 12 / 32 / 32 / 32. */
/* It had a 28rem measure of its own while it carried the launch date - at 54
   characters the inherited 22rem broke it with "30th." alone on a second
   line. Without the date it is 34 characters and fits the 22rem `.holding p`
   sets, so the override went with the date. */
.holding .stamp {
  margin-bottom: 2rem;
}

.holding .wordmark {
  color: var(--ink-muted);
  font-size: 1.75rem;
  line-height: inherit;
  margin: 0 0 0.75rem;
}

.holding p {
  color: var(--ink-muted);
  max-width: 22rem;
}

@media (max-width: 30rem) {
  .page {
    padding: 3rem 1.25rem 4rem;
  }

  h1 {
    font-size: 1.625rem;
  }
}

/* --- unfilled ---------------------------------------------------------- */

/*
 * A blank that only its owner can fill: a legal name, a postal address. Styled
 * to be impossible to miss rather than tastefully grey, because the failure
 * mode is deploying the page without noticing it is still empty.
 */
.todo {
  background: #ffe9a8;
  border: 1px dashed #b58e7a;
  border-radius: 4px;
  padding: 0.0625rem 0.375rem;
  font-family: 'Courier Prime', ui-monospace, monospace;
  font-size: 0.9375em;
  color: #6b4a12;
}

/* --- store link ------------------------------------------------------- */

/*
 * The App Store link in the hero. It took the place of the waitlist form at
 * launch, and it is that form's button carried over: same fill, same edge,
 * same size, so the hero still has exactly one object on it besides the mark.
 *
 * The app's accent button, and the same three tokens components/PillButton.tsx
 * reads: `accent` fills it, `accentEdge` draws the border, `accentLabel` is the
 * label - and accentLabel is ink, so the text on it is dark rather than light.
 *
 * ⚠️ **The border is not decoration.** tokens.json records that the accent is
 * only 2.27:1 against paper, so the fill alone does not describe the button's
 * shape; the darker edge is what makes it an object. Do not drop it to
 * "simplify" the rule.
 *
 * ⚠️ `.holding` is a flex column, and **margins do not collapse in a flex
 * container** - they add. The three lines above end with a 1rem bottom margin,
 * so 1rem here is what makes the 2rem gap the rest of the hero uses.
 *
 * The label is allowed to wrap. The form's button was `nowrap` because it
 * shared a row with the field; alone, a label that cannot wrap is one that
 * overflows a 320px screen at 200% text.
 */
.store {
  margin-top: 1rem;
  border: 1px solid var(--accent-edge);
  border-radius: 8px;
  background: var(--accent);
  color: var(--ink);
  font-size: 1rem;
  line-height: 1.4;
  padding: 0.625rem 1.25rem;
  /* 44px, the same minimum tap target as the footer links. */
  min-height: 2.75rem;
  text-decoration: none;
}

/* Restated rather than inherited: the site's `a:hover` changes the colour,
   and on this fill the label has to stay ink. */
.store:hover {
  background: var(--accent-edge);
  color: var(--ink);
}

/* No hover on a touch screen, so the press is the only feedback there is. */
.store:active {
  background: var(--accent-edge);
  transform: translateY(1px);
}

/* Focus is drawn rather than removed: it is the one control in the hero and
   it has to be reachable from a keyboard. */
.store:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

/* --- screenshots ------------------------------------------------------- */

/*
 * The App Store gallery, between the hero and the prose. Seven JPEGs baked
 * out of design/screenshots/ by design/bake-site-shots.py, in the STORE's
 * order rather than the file order - see that script's docstring.
 *
 * It is a SIBLING of the two `.page` sections rather than a child of one,
 * which is what lets it span the window with no negative margin and no
 * `100vw`. 100vw counts the scrollbar, so on a page that always has one it
 * would overhang by that width and add a horizontal scrollbar of its own.
 *
 * Scroll-snap and nothing else. The store's own gallery is a swipeable strip
 * and CSS does the whole thing, so there is still no JavaScript anywhere on
 * this site and still no `script-src` to add to the CSP.
 *
 * `--pad` is local to the gallery on purpose: it tracks `.page`'s horizontal
 * padding at both breakpoints so the first shot lines up with the text above
 * it, without this rule reaching into `.page` to say so.
 */
.shots {
  --pad: 1.5rem;

  overflow-x: auto;
  scroll-padding-inline: var(--pad);
  scroll-snap-type: x mandatory;
  /* Without this, flicking past the last shot rubber-bands the whole page. */
  overscroll-behavior-x: contain;
}

/* Focus is drawn rather than removed, exactly as on the store link. `tabindex="0"`
   in the markup is what lets a keyboard scroll this at all, and a focusable
   thing with no visible focus is worse than one that cannot be reached. */
.shots:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

.shots-strip {
  display: flex;
  gap: 0.75rem;
  max-width: 96rem;
  margin: 0 auto;
  padding: 0 var(--pad);
  list-style: none;
}

/* The trailing inset. Measured, not assumed: a scroll container's scrollable
   overflow takes in the border boxes of its descendants and stops there - it
   counts neither the trailing padding nor a trailing margin, so scrollWidth
   came back 24px short and the seventh shot ended flush against the edge
   where the first had its inset. This is a real box in the flow, which does
   count. Minus the gap, which is already laid before it. */
.shots-strip::after {
  content: '';
  flex: 0 0 calc(var(--pad) - 0.75rem);
}

/* `start`, not `center`. Mandatory snapping is applied at load as well as on
   scroll, and centre-aligned it would snap a strip that is sitting at zero to
   whichever shot is nearest the middle - the gallery would arrive already
   scrolled past its first frame. */
.shots-strip li {
  flex: 0 0 10.5rem;
  scroll-snap-align: start;
}

/*
 * The frames are baked on the app's paper, which is darker than the page's,
 * so they read as panels lying on it without needing a shadow. The hairline
 * is there only to keep the corner crisp where the two papers are closest.
 *
 * width and height are on the <img> as attributes too, so the strip reserves
 * its full height before a single lazy shot has arrived. Seven images popping
 * in one by one and shoving the page around is the layout shift this would
 * otherwise ship.
 */
.shots-strip img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 12px;
  border: 1px solid var(--divider);
  background: var(--surface);
}

/*
 * Once the window is wide enough to hold all seven, it shows all seven: the
 * shots stop being a fixed width and share the row instead. Below this they
 * stay a swipe, because seven across a phone is 45px each, which is a row of
 * thumbnails with no readable caption on any of them.
 */
@media (min-width: 68.75rem) {
  .shots-strip li {
    flex: 1 1 0;
    min-width: 0;
  }

  /* Nothing overflows at this width, so there is no trailing inset to fake -
     the list's own padding is doing the job again. */
  .shots-strip::after {
    display: none;
  }
}

@media (max-width: 30rem) {
  .shots {
    --pad: 1.25rem;
  }
}
