/* ==========================================================================
   Blip — tryblip.app
   One stylesheet for the whole site: the landing page and the two legal pages.

   Everything here is plain CSS. No build step, no preprocessor. Values that
   also exist in the app are commented with where they came from, so a change
   in the app can be traced to a change here.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Fonts

   Google Sans Flex, the app's typeface, shipped under the SIL Open Font
   License 1.1. The licence travels with the binaries in fonts/OFL.txt and
   must stay there.

   Only Regular (400) and Medium (500) exist. Nothing on this site asks for a
   heavier weight, and `font-synthesis: none` stops a browser from faking one.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "Google Sans Flex";
  src: url("fonts/GoogleSansFlex-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Google Sans Flex";
  src: url("fonts/GoogleSansFlex-Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* --------------------------------------------------------------------------
   Tokens
   -------------------------------------------------------------------------- */
:root {
  /* Brand, straight from Blip/DesignSystem/Tokens/BlipColor.swift */
  --cream: #fff3df;      /* background/cream */
  --orange: #ff9b67;     /* color/orange */
  --yellow: #ffbd24;     /* color/yellow */
  --ink: #000000;        /* color/black  */

  /* The app's secondary text is #747474, which measures 4.26:1 on cream —
     under the 4.5:1 that WCAG AA asks of body text. Nudged two steps darker
     to 4.58:1. Indistinguishable by eye, and it passes. */
  --grey: #6f6f6f;

  --shelf: #a7a197;      /* the 2pt line the app's presence balloons rise behind */

  /* The balloon family. Orange and yellow are the brand mark's own two.
     The other three are lifted from BlipPalette.avatarPalette, so every
     balloon on this page is a colour the app already uses. Each ink is the
     face colour that ships with the art, or a darkened tone of its own skin. */
  --skin-orange: #ff9b67;   --ink-orange: #7f0500;
  --skin-yellow: #ffbd24;   --ink-yellow: #7b3100;
  --skin-butter: #ffe167;   --ink-butter: #7b5200;
  --skin-sky: #67d6ff;      --ink-sky: #00465f;
  --skin-blossom: #ff67f0;  --ink-blossom: #5c0054;

  /* The demo card's surface and its ink. The card is the page's one raised
     object and it was cream on cream: measured 1.0000:1, the same colour.

     No fill fixes that by brightness. Cream's relative luminance is 0.9069,
     so the most contrast *any* lighter fill can reach against it is white's
     1.0973:1, and a surface only starts to read at about 1.2:1. What a fill
     buys here is warmth, not light: --card is the cream mixed three quarters
     of the way to white, same hue and saturation (hsl(37.5 100% 98.4%)
     against the cream's 93.7%), which measures 6.6 dE2000 from it -- nearly
     three times the ~2.3 dE that is just noticeable. Pure white was rendered
     and compared: the last 28% of the available light costs all of the
     warmth and reads as a card from another design system.

     --card-ink draws the border and the shadow, and is --ink-butter halved
     into black, so both darken the cream along the cream's own hue. At the
     shadow's densest point it measures hue 85.2deg against the cream's 86.0
     at 131% of its chroma; neutral black there is 83% and a cool grey 19% --
     a desaturated patch under a warm card is the tell that the card was
     dropped in from somewhere else. It is a channel triplet because each use
     needs its own alpha. */
  --card: #fffcf7;
  --card-ink: 62, 41, 0;

  /* Type. The app tracks every style at -0.025em; the line-height scale
     rounds to about 1.27. */
  --tracking: -0.025em;

  /* The shell is wide and the gutter is thin, so on a desktop the text sits
     near the window edge instead of in a phone-width column adrift in cream.
     --measure caps the line length of a paragraph inside it; the grids do
     that job themselves for the columns they hold, and a heading or a row of
     balloons takes the whole width.

     --space is the gutter. 1.25rem on a phone, unchanged; 2rem rather than
     2.5rem at the top end, which brings a 1280 or 1440 window's text 8px
     closer to the edge. */
  --measure: 34rem;
  --shell: 90rem;
  --space: clamp(1.25rem, 5vw, 2rem);

  /* The vertical unit. Every gap down the page that is bigger than a line of
     text is this, or 1.5, 2 or 2.5 of it — nothing else — so the rhythm
     repeats instead of being re-guessed per section:

       1x    grid items, wordmark air, the figure to its caption
       1.5x  the footer band, the balloon row to the headline
       2x    a section's heading to its own content
       2.5x  a section's own padding, so two sections meet at 5x

     24 on a phone, 40 from 1000px up. Gaps smaller than 1x are set in rem
     straight from a 8 / 16 / 32 scale, because the distance between a title
     and its own paragraph is a fact about the text, not about the viewport.

     This is deliberately not --space: the page wants a thinner gutter and a
     taller rhythm, and since the dividers came out the section boundary is
     the only thing separating one section from the next. */
  --rhythm: clamp(1.5rem, 4vw, 2.5rem);
}

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

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

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: "Google Sans Flex", -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, system-ui, sans-serif;
  font-weight: 400;
  font-synthesis: none;
  letter-spacing: var(--tracking);
  font-size: 1.0625rem; /* 17px: the app's 15pt body, at web reading distance */
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
h4 {
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: -0.03em;
  margin: 0;
  text-wrap: balance;
}

/* Leading tracks size inversely, so 1.15 above is display leading and belongs
   to the 28-68px headings. A 20px heading in a grid cell is text-sized: at
   1.15 a wrapped one sets tighter than the paragraph under it, which is
   backwards. Nothing wraps at the default font size; a reader who has scaled
   their text up is where this shows. */
h3,
h4 {
  line-height: 1.3;
}

p {
  margin: 0;
}

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

a {
  color: var(--ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

a:hover {
  text-decoration-thickness: 2px;
}

/* A black ring reads 19:1 on cream. Orange would be 1.9:1 and fail. */
:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: 0.75rem 1rem;
  background: var(--orange);
  color: var(--ink);
  z-index: 10;
}

.skip-link:focus {
  left: 0.5rem;
  top: 0.5rem;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   Layout
   -------------------------------------------------------------------------- */
.wrap {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--space);
}

/* 2.5x top and bottom, so where two sections meet the boundary is 5x — 120 on
   a phone, 200 at 1440. With no divider left to mark it, that gap is the only
   thing saying "new section", so it has to beat every gap inside one (the
   largest of those is 2x, a heading to its content) by a clear margin. */
.section {
  padding-block: calc(var(--rhythm) * 2.5);
}

.rule {
  height: 2px;
  border: 0;
  margin: 0;
  background: var(--shelf);
  opacity: 0.35;
}

.section__title {
  font-size: clamp(1.75rem, 7vw, 2.5rem);
}

/* --------------------------------------------------------------------------
   Wordmark
   -------------------------------------------------------------------------- */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1.375rem;
  font-weight: 500;
  letter-spacing: -0.04em;
  text-decoration: none;
}

.brand__mark {
  width: 1.55rem;
  height: auto;
}

.masthead {
  padding-top: var(--rhythm);
}

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */
/* The bottom is a full section padding, not a smaller one: it is one half of
   the hero/demo boundary and the section below contributes the other half, so
   the two read as the same size. They used to be 72 and 104. */
.hero {
  /* **1rem, not a full rhythm.** The masthead contributes `--rhythm` of air
     above the wordmark and this used to contribute another below it, so the
     wordmark and the headline sat 40px apart while everything under the
     headline is drawn from the text scale — 8 to the lede, 32 to the button,
     16 to the fine print. The top of the page read as loose and the rest as
     tight, which is the owner's report. The distance from a wordmark to the
     title beneath it is a fact about the text, so the scale above says to take
     it from the 8 / 16 / 32 rem set rather than from the viewport rhythm. */
  padding-top: 1rem;
  padding-bottom: calc(var(--rhythm) * 2.5);
}

.hero__grid {
  display: grid;
  gap: 0;
}

.hero__title {
  font-size: clamp(2.5rem, 11.5vw, 4.25rem);
  max-width: 18ch;
}

/* 8px, which reads as about 27. The h1's line box carries 9px of air below
   its descenders and this one carries 9px above its caps, so the visible gap
   is the declared one plus 18. At the old 20 the lede sat 39px under the
   headline and only 46 above the badge — nearly as far from the thing it
   belongs to as from the thing it does not. */
.hero__lede {
  margin-top: 0.5rem;
  max-width: var(--measure);
  font-size: clamp(1.0625rem, 4.4vw, 1.3125rem);
  line-height: 1.45;
}

.hero__lede strong {
  font-weight: 500;
}

.hero__cta {
  margin-top: 2rem;
}

.hero__fineprint {
  margin-top: 1rem;
  color: var(--grey);
  font-size: 0.9375rem;
}

/* The brand lockup, rebuilt so each balloon is its own element and can react
   on its own. The numbers are the two balloons' placement inside
   blip-logo-lockup.pdf (150 x 118.883), expressed as percentages so the pair
   keeps the composition at any size.

   The width and the block margin are phone-only: the @media below replaces both.
   At 42vw the lockup stood 125px tall with another 45 of margin, which put the
   App Store badge 610px down a 375x667 iPhone SE — past the ~553px Safari shows
   before its own bars scroll away, and past the 568px viewport of the narrower
   phones entirely. 32vw and a tighter margin bring the badge to 559 and 546. */
.pair {
  display: block;
  position: relative;
  width: clamp(7.5rem, 32vw, 14rem);
  aspect-ratio: 150 / 118.883;
  margin-block: clamp(0.75rem, 3.5vw, 2.25rem);
}

.pair .balloon {
  position: absolute;
}

.pair .balloon--a {
  left: 0.21%;
  top: 13.18%;
  width: 50.25%;
}

.pair .balloon--b {
  left: 47.75%;
  top: 0;
  width: 52.25%;
}

/* On a wide screen the pair sits beside the headline; on a phone it sits above
   it, right under the wordmark, which is where the brand mark belongs anyway.

   This block has to come after .pair's own rules: it overrides the width and
   the margin they set, and both selectors are one class deep. */
@media (min-width: 48rem) {
  .hero__grid {
    grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
    align-items: center;
    gap: calc(var(--rhythm) * 1.5);
  }

  /* Stretched, not justified to the end: .pair's width is a percentage of
     this column, and a shrink-to-fit column would resolve that to zero. The
     auto margin is what pushes the art to the right edge instead. */
  .hero__art {
    grid-column: 2;
    grid-row: 1;
  }

  .hero__copy {
    grid-column: 1;
    grid-row: 1;
  }

  .pair {
    width: min(100%, 22rem);
    margin: 0 0 0 auto;
  }
}

/* --------------------------------------------------------------------------
   The App Store badge

   Apple's own artwork, downloaded from developer.apple.com and unmodified.
   Their guidelines forbid redrawing it, recolouring it, or squashing it, so
   the only thing set here is a height.
   -------------------------------------------------------------------------- */
.appstore {
  display: inline-block;
  border-radius: 8px;
  line-height: 0;
}

.appstore img {
  height: 3.5rem;
  width: auto;
}

.appstore:hover img {
  opacity: 0.85;
}

/* --------------------------------------------------------------------------
   The live-typing demo

   Deliberately built the way the app builds a transcript: no bubbles. A
   message is bare text, yours to the right and theirs to the left, with a
   10pt grey "Sent" under the last of your run. An unsent draft is the same
   text at 60% opacity with a caret after it — which is exactly what
   Blip/Features/Chat/ChatTranscript.swift draws.
   -------------------------------------------------------------------------- */
.demo {
  margin: 0;
}

.chat {
  max-width: 26rem;
  margin-top: calc(var(--rhythm) * 2);
  padding: 1rem 1.125rem 1.375rem;
  background: var(--card);
  /* Still 2px, so the card's height is untouched -- it has to hold 202px
     through the whole typing cycle, and only the colour moves. A translucent
     border composites over the element's own background rather than over the
     page (background-clip is border-box), so this renders #d5cdc0: 1.4365:1
     against the page, against the 1.1761:1 of the rgba(0, 0, 0, 0.07) it
     replaces. The same alpha of black renders #c7c5c1, a grey line on a warm
     card. */
  border: 2px solid rgba(var(--card-ink), 0.22);
  border-radius: 24px; /* BlipMetrics confirmationCornerRadius */
  /* Contact, then lift. The 1px layer keeps the card on the page instead of
     hovering free of it; the wide layer is the elevation, and it is the
     largest of the three cues -- measured, the ground peaks at 1.6115:1 just
     below the card and is back to the page colour 39.5px down. The -10px
     spread holds the wide layer's tail *above* the card to 1.0622:1: a
     shadow above a card lit from above is a mistake you can measure. */
  box-shadow:
    0 1px 2px rgba(var(--card-ink), 0.12),
    0 16px 32px -10px rgba(var(--card-ink), 0.26);
}

/* Asked for more contrast, given a border that clears the 3:1 WCAG 1.4.11
   wants of a graphical object (measured 3.17:1 against the page). The
   default 1.44:1 does not, and is not claimed to: the outline carries
   nothing the transcript and its caption do not already say. Nothing here is
   translucent over the page or blurred, so prefers-reduced-transparency has
   nothing to answer -- every alpha composites over the card's own fill. */
@media (prefers-contrast: more) {
  .chat {
    border-color: rgba(var(--card-ink), 0.55);
  }
}

.chat__top {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding-bottom: 0.875rem;
  /* 0.07 of the card ink renders #f1ede5, which is the weight
     rgba(0, 0, 0, 0.06) carried on cream (1.1409:1 against the fill, against
     its 1.1444:1) at C*ab 4.34 instead of 2.77. Black over cream kept the
     cream's chroma; black over a near-white fill has almost none to keep. */
  border-bottom: 2px solid rgba(var(--card-ink), 0.07);
}

.chat__avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  background: var(--skin-butter);
  display: grid;
  place-items: center;
  font-size: 0.875rem;
  font-weight: 500;
  color: rgba(0, 0, 0, 0.65);
}

.chat__name {
  font-size: 0.9375rem;
  font-weight: 500;
}

.chat__log {
  list-style: none;
  margin: 0;
  padding: 1rem 0 0;
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  min-height: 7rem;
  font-size: 1rem; /* the app's 16pt message body */
  line-height: 1.25;
}

.msg {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

/* Holds the text and the caret on one line: .msg itself is a column, so the
   caret has to share an inline context with the words it follows. */
.msg__line {
  display: inline;
}

.msg--mine {
  align-items: flex-end;
  text-align: right;
}

.msg--theirs {
  align-items: flex-start;
}

.msg__receipt {
  font-size: 0.625rem; /* 10pt receipt */
  color: var(--grey);
}

/* An unsent draft: same face, same size, same colour, less solid.
   BlipMetrics.Chat.liveDraftOpacity is 0.6, so this is 0.6. */
.msg__text {
  transition: opacity 260ms ease-out;
}

.msg--live .msg__text {
  opacity: 0.6;
}

.caret[hidden] {
  display: none;
}

.caret {
  display: inline-block;
  width: 2px;
  height: 1.05em;
  margin-left: 1px;
  vertical-align: -0.16em;
  background: var(--ink);
  opacity: 0.6;
  animation: caret 1.06s steps(1) infinite;
}

@keyframes caret {
  0%,
  49% {
    opacity: 0.6;
  }
  50%,
  100% {
    opacity: 0;
  }
}

/* Centers a whole section, title and all. Used by the demo, whose card is the thing
   being looked at rather than read past. */
.section--centered {
  text-align: center;
}

.section--centered .demo,
.section--centered .chat,
.section--centered .demo__caption {
  margin-inline: auto;
}

/* The section's heading centres and the cells centre as a set, including a
   last row that does not fill; the words inside a cell do not. A 3 to 6 line
   paragraph centred in a 427px cell gives every line a different left edge,
   and it also hides the column gap — the space between two columns of centred
   text varies line by line, so a grid of it reads as unspaced. Straight left
   edges are also the only structural signal left now that the dividers are
   gone. One rule here replaces two exceptions that used to fight it. */
.section--centered .cards,
.section--centered .qa {
  text-align: left;
}

.demo__caption {
  margin-top: var(--rhythm);
  /* One line wherever one line fits, and no arbitrary width to make that
     happen: `max-content` is exactly the unwrapped sentence, and the 100% cap
     hands it back to normal wrapping the moment the shell is narrower than
     that — which is every phone, where 61 characters cannot sit on one line at
     17px whatever we do. A fixed max-width in rem would have been a guess that
     breaks when the copy or the font changes; this cannot be off by one word.

     It used to be capped at the card's own 26rem so the two shared a left and
     a right edge. That is the alignment being given up here: at 1280 the
     caption is wider than the card it captions.

     1.6 is looser than the body's 1.55 and tighter than the 1.8 it had while
     it was three sentences — at 1.8 the two halves of one sentence sat 15px
     apart and read as two. */
  width: max-content;
  max-width: 100%;
  line-height: 1.6;
  /* Only bites on a phone, where it does wrap: left to itself the sentence
     broke three words from the end and hung "take it back." on its own. */
  text-wrap: balance;
  color: var(--grey);
}

.demo__caption b {
  color: var(--ink);
  font-weight: 500;
}

/* The caption sits under the card at every width — this block only makes it
   bigger. The comment that used to be here said it sat beside the card, which
   stopped being true when the two-column version was replaced by a centred
   stack, and two dead declarations went with it: a duplicate `max-width: 34ch`
   and a `margin-top: 0` that, once the card and caption stacked, put the
   caption flush against the card's bottom border with nothing between them. */
@media (min-width: 62rem) {
  /* The one honest sentence on the page, so once there is room for it the
     caption gets a little more size than a caption normally would. */
  .demo__caption {
    font-size: 1.1875rem;
  }
}

/* --------------------------------------------------------------------------
   Feature list and Q&A

   Two lists, one behaviour: a single column on a phone, two from a tablet up.
   Two, not three, because each list holds four items -- three columns left a
   single card centred under a row of three, which reads as a leftover. Two
   columns divide four evenly at every width above the phone, and the owner
   picked that over a row of four (each cell would be ~280px, about 34
   characters a line, and these paragraphs would run to eight lines).

   The cell is capped at --measure, the same cap the page's loose paragraphs
   take. Without it a cell is 588px at 1280 and 660px at 1440 -- 75 and 85
   characters, past the top of the comfortable range -- and the cap is the
   reason the pair sits centred rather than reaching the gutters, which is the
   one place this layout gives up the page's usual full-width behaviour.

   A wrapping flex row rather than a grid, because a short last row has to
   centre under a centred heading, and `justify-content` cannot do that to a
   grid: with 1fr tracks there is no free space to distribute, and with fixed
   tracks it moves the whole track set rather than the short row. The basis is
   the column width exactly, grow is 0 so an orphan keeps it, and
   justify-content centres it. Four items no longer produce an orphan, but a
   fifth would, and this keeps working if one comes back.
   -------------------------------------------------------------------------- */
.cards,
.qa {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--rhythm);
  margin-top: calc(var(--rhythm) * 2);
}

.cards > *,
.qa > * {
  flex: 0 1 100%;
}

@media (min-width: 48rem) {
  .cards > *,
  .qa > * {
    flex-basis: calc((100% - var(--rhythm)) / 2);
    max-width: var(--measure);
  }
}

/* One size for a cell heading, in either list. They were 20 and 18. */
.card__title,
.qa h3 {
  font-size: 1.25rem;
}

.card__title {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.card__dot {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 999px;
  background: var(--orange);
  translate: 0 -0.15em;
}

.card p {
  margin-top: 0.5rem;
  color: var(--grey);
}

/* --------------------------------------------------------------------------
   Straight answers
   -------------------------------------------------------------------------- */
.qa p {
  margin-top: 0.5rem;
  color: var(--grey);
}

.qa a {
  color: var(--grey);
}

/* --------------------------------------------------------------------------
   Closing call to action
   -------------------------------------------------------------------------- */
.close {
  text-align: center;
}

.close__title {
  font-size: clamp(1.875rem, 8vw, 2.75rem);
  max-width: 24ch;
  margin-inline: auto;
}

.close__cta {
  margin-top: 2rem;
}

.close__note {
  margin-top: 1rem;
  color: var(--grey);
  font-size: 0.9375rem;
}

/* A loose bunch. Sizes and offsets are hand-set rather than generated, so the
   row reads as a group of characters instead of a chart. */
.bunch {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: clamp(0.75rem, 4vw, 1.75rem);
  margin-bottom: calc(var(--rhythm) * 1.5);
}

.bunch .balloon {
  width: clamp(2.75rem, 12vw, 3.5rem);
}

.bunch .balloon:nth-child(2) {
  width: clamp(3.25rem, 14vw, 4.25rem);
  margin-bottom: 1.5rem;
}

.bunch .balloon:nth-child(3) {
  width: clamp(4rem, 17vw, 5rem);
}

.bunch .balloon:nth-child(4) {
  width: clamp(3.25rem, 14vw, 4.25rem);
  margin-bottom: 0.9rem;
}

/* --------------------------------------------------------------------------
   Balloons

   Two elements per balloon on purpose: .balloon carries the idle float and
   .balloon__art carries the reaction, so a hover never fights the drift.
   -------------------------------------------------------------------------- */
.balloon {
  display: block;
  animation: float var(--dur, 7s) ease-in-out var(--delay, 0s) infinite;
  transform-origin: 50% 90%;
}

/* Art is not text. Without this, a tap on a balloon or on the demo's avatar
   starts a selection and iOS offers to copy it. Both inherit to the SVG. */
.balloon,
.chat__avatar {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

.balloon__art {
  width: 100%;
  height: auto;
  overflow: visible;
  cursor: pointer;
  transform-origin: 50% 92%;
  transition: transform 220ms cubic-bezier(0.34, 1.4, 0.64, 1);
  -webkit-tap-highlight-color: transparent;
}

@keyframes float {
  0%,
  100% {
    transform: translateY(0) rotate(var(--tilt, 0deg));
  }
  50% {
    transform: translateY(-7px) rotate(calc(var(--tilt, 0deg) + 1.6deg));
  }
}

.balloon__body {
  fill: var(--skin, var(--orange));
}

.balloon__face {
  fill: var(--face, var(--ink-orange));
}

.balloon__eye,
.balloon__mouth {
  transition: transform 170ms ease-out;
}

/* Eye and mouth centres, in the viewBox's own units. SVG resolves a length
   here against the view box, so these are the measured centres of the art. */
.balloon--a .balloon__eye--1 { transform-origin: 19.885px 20.302px; }
.balloon--a .balloon__eye--2 { transform-origin: 28.461px 17.395px; }
.balloon--a .balloon__mouth { transform-origin: 25.377px 22.548px; }

.balloon--b .balloon__eye--1 { transform-origin: 7.481px 28.182px; }
.balloon--b .balloon__eye--2 { transform-origin: 18.295px 28.998px; }
.balloon--b .balloon__mouth { transform-origin: 12.332px 33.738px; }

/* A blink. Squashing the eye to a line is enough; nothing else moves. */
.balloon.is-blinking .balloon__eye {
  transform: scaleY(0.1);
  transition-duration: 70ms;
}

/* Noticed. Eyes widen, mouth opens, and the balloon leans in. */
.balloon.is-curious .balloon__art {
  transform: translateY(-5px) scale(1.035);
}

.balloon.is-curious .balloon__eye {
  transform: scale(1.14);
}

.balloon.is-curious .balloon__mouth {
  transform: scale(1.3);
}

/* Poked. */
.balloon.is-poked .balloon__art {
  animation: boing 520ms cubic-bezier(0.22, 1, 0.36, 1);
}

.balloon.is-poked .balloon__mouth {
  transform: scale(1.45);
}

@keyframes boing {
  0% {
    transform: scale(1, 1);
  }
  28% {
    transform: scale(1.09, 0.9) translateY(2px);
  }
  58% {
    transform: scale(0.95, 1.07) translateY(-6px);
  }
  82% {
    transform: scale(1.02, 0.98) translateY(0);
  }
  100% {
    transform: scale(1, 1);
  }
}

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */
.footer {
  padding-block: calc(var(--rhythm) * 1.5);
  font-size: 0.9375rem;
  color: var(--grey);
}

.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  padding: 0;
  margin: 0;
  list-style: none;
}

/* These are standalone links in a row, not links inside a sentence, so they get
   a real touch target rather than the 18px an inline box reports. 13px of
   padding either side of the 23.3px line box measures 49.3 tall; the matching
   negative margin takes the padding back out of the layout, so the footer looks
   exactly as it did and only the hit area grows. Measured before this: 46x18,
   39x18, 132x18 — those are font boxes, which is why the total is 49.3 and not
   the 44 that 18 + 26 would suggest. */
.footer__links a {
  color: var(--ink);
  display: inline-block;
  padding: 0.8125rem 0.375rem;
  margin: -0.8125rem -0.375rem;
}

/* Links left, copyright right, centered against each other. One line each way, so
   a row reads better than a stack and the wide shell has somewhere to put them. */
@media (min-width: 48rem) {
  .footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 2rem;
  }

  /* Widen the row gap so a wrapped row clears the one above it. 48rem used to
     drop the padding outright, on the reasoning that "a mouse does not need the
     bigger target" — but 768px is an iPad in portrait, not a mouse, and it took
     the footer's touch targets down to 23px there. The overlap that reasoning
     was avoiding is real: two wrapped rows sat 8px apart and hit boxes 13px
     taller than their text overlapped by 18px. Giving the rows 2.125rem of gap
     (8px + the 13px each box adds above and below) separates them instead, so
     the target survives on touch. */
  .footer__links {
    row-gap: 2.125rem;
  }

  /* Only a real pointer gets the tight row. */
  @media (hover: hover) and (pointer: fine) {
    .footer__links {
      row-gap: 0.5rem;
    }

    .footer__links a {
      padding: 0;
      margin: 0;
    }
  }
}

.footer__meta {
  margin: 1rem 0 0;
  max-width: 46ch;
}

@media (min-width: 48rem) {
  .footer__meta {
    margin: 0;
    text-align: right;
  }
}

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

   A document shell: one column, generous measure, headings that can be
   scanned. Generated by build-legal.mjs from docs/legal/*.md — do not edit
   terms/index.html or privacy/index.html by hand.
   -------------------------------------------------------------------------- */
/* The document column. Split in two so the masthead and the footer can share
   the measure without also inheriting the body's bottom padding.

   Wider than --measure because these pages carry tables and numbered
   clauses, and narrower than .wrap because they are documents: past about
   this width a legal paragraph is harder to read, not easier. */
.doc-col {
  max-width: 46rem;
  margin-inline: auto;
  padding-inline: var(--space);
}

.doc {
  padding-bottom: calc(var(--rhythm) * 2.5);
}

.doc__back {
  display: inline-block;
  margin-top: 0.75rem;
  color: var(--grey);
  font-size: 0.9375rem;
}

.doc__body {
  margin-top: var(--rhythm);
  /* The documents quote database paths. A long one must wrap rather than push
     the whole page sideways on a phone. */
  overflow-wrap: break-word;
}

.doc__body h1 {
  font-size: clamp(2rem, 8vw, 2.75rem);
}

.doc__body h2 {
  font-size: 1.5rem;
  margin-top: 3rem;
}

.doc__body h3 {
  font-size: 1.1875rem;
  margin-top: 2rem;
}

.doc__body h4 {
  font-size: 1.0625rem;
  margin-top: 1.5rem;
}

.doc__body p,
.doc__body ul,
.doc__body ol {
  margin-top: 1rem;
}

.doc__body li + li {
  margin-top: 0.5rem;
}

.doc__body ul,
.doc__body ol {
  padding-left: 1.25rem;
}

.doc__body strong {
  font-weight: 500;
}

.doc__body a {
  color: var(--ink);
}

.doc__body hr {
  height: 2px;
  border: 0;
  background: var(--shelf);
  opacity: 0.35;
  margin-block: 3rem;
}

.doc__body code {
  overflow-wrap: break-word;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875em;
  background: rgba(0, 0, 0, 0.05);
  padding: 0.1em 0.35em;
  border-radius: 5px;
}

.doc__body pre {
  overflow-x: auto;
  background: rgba(0, 0, 0, 0.05);
  padding: 1rem;
  border-radius: 12px;
}

.doc__body pre code {
  background: none;
  padding: 0;
}

/* A blockquote carries the drafts' own "not legal advice" notices. */
.doc__body blockquote {
  margin: 1.5rem 0;
  padding: 1rem 1.125rem;
  border-left: 4px solid var(--orange);
  background: rgba(255, 155, 103, 0.14);
  border-radius: 0 12px 12px 0;
}

.doc__body blockquote > :first-child {
  margin-top: 0;
}

.doc__table {
  overflow-x: auto;
  margin-top: 1rem;
}

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

.doc__body th,
.doc__body td {
  text-align: left;
  vertical-align: top;
  padding: 0.625rem 0.75rem;
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}

.doc__body th {
  font-weight: 500;
  border-bottom-width: 2px;
}

.doc__toc {
  margin-top: 2rem;
  padding: 1.25rem 1.375rem;
  border: 2px solid rgba(0, 0, 0, 0.08);
  border-radius: 18px;
}

.doc__toc h2 {
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--grey);
  margin: 0;
}

/* No markers: every section heading in these documents carries its own
   number, and a list marker beside it reads as two numbering schemes. */
.doc__toc ul {
  list-style: none;
  margin: 0.75rem 0 0;
  padding-left: 0;
  font-size: 0.9375rem;
}

.doc__toc li + li {
  margin-top: 0.5rem;
}

.doc__toc a {
  color: var(--ink);
  text-decoration-color: rgba(0, 0, 0, 0.3);
}

/* --------------------------------------------------------------------------
   Reduced motion

   Everything decorative stops. The caret stops blinking and stays visible,
   and script.js reads the same media query to leave the demo on its final
   frame rather than typing it out.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  .balloon,
  .balloon__art,
  .balloon__eye,
  .balloon__mouth,
  .caret,
  .msg__text {
    animation: none !important;
    transition: none !important;
  }

  .caret {
    opacity: 0.6;
  }

  .balloon__art {
    cursor: default;
  }
}
