/* Loyollo — marketing site
   Direction: Loyollo's own gradient as the ground, black ink, the stamp
   artwork drifting behind everything, content on frosted white.
   Matches the app's sign-in screen — see docs/design-direction.md */

:root {
  color-scheme: light;
  --ink:        #120D0B;
  --ink-dim:    rgba(18, 13, 11, 0.66);
  --ink-faint:  rgba(18, 13, 11, 0.42);
  --line:       rgba(18, 13, 11, 0.14);
  --paper-solid:#FFFFFF;

  /* Loyollo's own: white → pink → blue, as on the inactive cards and the
     app's sign-in screen. */
  --house: linear-gradient(150deg, #FFFFFF 0%, #FCD3FF 52%, #8ECAFF 100%);

  --r-lg: 28px;
  --r-md: 20px;
  --r-sm: 14px;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  min-height: 100vh;
  color: var(--ink);
  font-family: "Bricolage Grotesque", Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  background: transparent;
}

/* The gradient belongs to the window, not to the document — a long page
   shouldn't run off the bottom of its own background. A fixed layer rather
   than `background-attachment: fixed`, which iOS Safari has never handled
   properly and which leaves everything below the first screen unpainted. */
html { background: #F5EDF8; }
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1;
  background: var(--house);
}

/* Width is a token because the pages want different measures. Everything with
   page furniture — the marketing pages, the legal ones — opts into .wide, so
   one left edge runs through the whole site; what differs is how wide the
   WORDS are allowed to get inside that, which is .prose's job rather than this
   one. s.html keeps the default: it is a single decision on a phone and has no
   furniture to line up with. */
.wrap { max-width: var(--wrap, 760px); margin: 0 auto; padding: 0 20px; position: relative; z-index: 1; }
.wrap.wide { --wrap: 1080px; }

/* A wide page buys room for the card grids and spends it on line length. At
   1080 the hero lead ran to 78 characters and the full-width panels to 93,
   against a comfortable 60-75, so the prose that is NOT in a grid gets capped.
   The cards cap themselves: two columns of 1080 is a 63-character line.

   Capped in em, not ch. Bricolage's zero is unusually wide — 0.65em against an
   average character of 0.47em — so a ch cap lands about 38% looser than it
   reads, which is how the 78 happened: `56ch` resolved to 760px. Measured with
   the font's own metrics over the actual sentences, not assumed. */
.wide .hero p  { max-width: 30em; }   /* ~65 characters at 21px */
.wide .block p { max-width: 32em; }   /* ~67 characters at 17px */

/* ——— the tap, drawn ———
   A fixed square so the still and the animation occupy exactly the same space:
   the swap happens under the reader's eyes and the headline must not move. The
   art is transparent line work, so it sits straight on the gradient with no
   panel behind it. */
/* 1190x1100 — the ink box, measured across all 90 frames rather than the square
   canvas the art was exported on. */
.tap-anim { position: relative; width: min(320px, 76vw); aspect-ratio: 1 / 1; margin: 6px 0 2px; }
/* No longer full-bleed on phones. That was right for the old illustration,
   which was wide and short and filled an edge-to-edge band; this drawing is
   square, so edge-to-edge meant a 390x390 box holding a card about 150px tall
   and roughly 240px of nothing, which pushed the headline and the button down
   the screen. On a page somebody looks at for four seconds before deciding
   whether to install something, that space is the most expensive thing on it. */
@media (max-width: 560px) {
  .tap-anim { margin-top: 2px; margin-bottom: 0; }
}
/* Short screens give the art less room. On an iPhone SE the square hero at its
   normal size leaves "Get Loyollo" 7px under the fold, which is the worst
   possible place for it — far enough to be invisible, near enough that nobody
   scrolls to look. 53px off the picture puts the button on the first screen. */
@media (max-height: 700px) {
  .tap-anim { width: min(260px, 62vw); }
}
/* Full width on every phone, short ones included — the picture does not give
   way to fit the fold. On an iPhone SE that puts "Get Loyollo" just below it,
   which is a scroll rather than a dead end.

   Square, because the art is now one of the feature spots, which are drawn on
   a square canvas and centred in it. The previous illustration needed a hand
   -picked crop (1190x1100 out of a taller canvas) to avoid floating in dead
   space; this one does not, and a crop here would only risk the still and the
   animation framing differently. */
.tap-anim img, .tap-anim .tap-anim-play { position: absolute; inset: 0; width: 100%; height: 100%; }
.tap-anim img { object-fit: contain; }
.tap-anim svg { display: block; }

/* ——— type ——— */
h1, h2, h3 {
  font-family: "Bricolage Grotesque", Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 800;
  font-variation-settings: "opsz" 48, "wght" 800;
  letter-spacing: -0.035em;
  text-wrap: balance;
  margin: 0;
}
h1 { font-size: clamp(40px, 10.5vw, 68px); line-height: 0.98; }
h2 { font-size: clamp(26px, 6vw, 38px); line-height: 1.05; }
h3 { font-size: 19px; line-height: 1.2; letter-spacing: -0.02em;
     font-variation-settings: "opsz" 14, "wght" 750; }
p  { margin: 0; color: var(--ink-dim); }
.lead { font-size: clamp(18px, 4.4vw, 21px); color: var(--ink); line-height: 1.45; }
.small { font-size: 14px; }
a { color: inherit; }

/* The other language, beside the brandmark. Quiet, but never hidden behind a
   menu: somebody who wants Welsh should not have to go looking for it. */

/* ——— chrome ——— */
nav {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding-block: 24px;
}
.brandmark { height: 26px; width: auto; display: block; }
.nav-link {
  color: var(--ink); text-decoration: none; font-size: 14px; font-weight: 650;
  border: 1px solid var(--line); border-radius: 999px; padding: 9px 16px;
  white-space: nowrap; background: rgba(255, 255, 255, 0.5);
}
.nav-link:hover { border-color: var(--ink-dim); }
.nav-right { display: flex; align-items: center; gap: 16px; }

/* EN / CY as plain links rather than a pill. Two languages is a pair of words,
   not a control — a pill the size of "Get the app" implied the language was
   the thing on this page worth pressing. The current language is marked with
   aria-current and carries full ink; the other is a link. */
.lang { display: inline-flex; align-items: center; gap: 7px;
        font-size: 14px; font-weight: 700; letter-spacing: 0.03em; }
.lang [aria-current] { color: var(--ink); }
.lang a { color: var(--ink-dim); text-decoration: none; }
.lang a:hover, .lang a:focus-visible { color: var(--ink); text-decoration: underline;
                                       text-underline-offset: 3px; }
.lang-sep { color: var(--ink-faint); font-weight: 400; }

main { padding-bottom: 8px; }
section { margin-block: clamp(44px, 9vw, 76px); }
.hero { margin-top: clamp(18px, 5vw, 38px); margin-bottom: clamp(26px, 5vw, 38px); }

.hero h1 { margin-bottom: 18px; }

/* The cycling trade in the headline. See swap.js.
   The width is animated rather than left to reflow: at up to 68px a word
   changing length shunts the rest of the sentence sideways, which reads as a
   glitch rather than as motion. The word itself crosses over vertically while
   the width eases underneath it, so the line appears to make room. */
.swap {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  transition: width 420ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.swap i {
  font-style: inherit; display: inline-block; white-space: pre;
  transition: opacity 260ms ease, transform 260ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.swap.out i { opacity: 0; transform: translateY(-0.42em); }
.swap.in  i { opacity: 0; transform: translateY(0.42em); transition: none; }
@media (prefers-reduced-motion: reduce) {
  .swap, .swap i { transition: none; }
}

.chip {
  display: inline-block; font-size: 12px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  padding: 6px 12px; border-radius: 999px; margin-bottom: 18px;
  background: var(--ink); color: #FFFFFF;
}

/* ——— buttons ——— */
.btn {
  display: inline-block; text-decoration: none; font-weight: 700; font-size: 16px;
  padding: 15px 26px; border-radius: 999px; border: 1px solid transparent;
  background: var(--ink); color: #FFFFFF; white-space: nowrap;
}
.btn.ink { background: var(--ink); color: #FFFFFF; }
.btn.ghost { background: rgba(255, 255, 255, 0.55); color: var(--ink); border-color: var(--line); }
.btn:hover { opacity: 0.88; }
.btn-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 26px; }

/* ——— the two audiences ——— */
.tabs {
  display: flex; gap: 6px; padding: 5px; margin-bottom: clamp(28px, 6vw, 44px);
  background: rgba(255, 255, 255, 0.55); border: 1px solid var(--line);
  border-radius: 999px; width: fit-content; max-width: 100%;
}
.tab {
  appearance: none; border: 0; cursor: pointer; white-space: nowrap;
  font: inherit; font-size: 15px; font-weight: 650;
  padding: 10px 20px; border-radius: 999px;
  background: transparent; color: var(--ink-dim);
}
.tab[aria-selected="true"] { background: var(--ink); color: #FFFFFF; }
.tab:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
[role="tabpanel"][hidden] { display: none; }

/* ——— content sits on frosted white, so the drift never fights the words ——— */
.panel {
  /* Solid white, not the 72% frost. The frost let the page gradient through, so
     a panel took a different tint depending on where it sat on the page and the
     artwork inside one — which is on white — read as a pasted rectangle. The
     backdrop-filter goes with it: with nothing translucent left to blur it was
     only forcing a compositing layer. */
  background: var(--paper-solid); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: clamp(20px, 5vw, 30px);
}
.panel + .panel { margin-top: 14px; }
.panel h2 { margin-bottom: 12px; }
.panel h3 + p { margin-top: 6px; }
.panel ul { margin: 0; padding-left: 20px; color: var(--ink-dim); }
.panel li + li { margin-top: 10px; }
.block { margin-top: 26px; }

.steps { display: grid; gap: 12px; grid-template-columns: repeat(3, 1fr); }
/* Inverted: solid ink, white type. These are the one thing on the page that
   is not frosted white, which is the point — they read as the spine of the
   section rather than three more panels, and they pick up the same ink as
   .btn.ink at the bottom of the block, so the eye goes steps -> button.

   No backdrop-filter here. It does nothing behind an opaque fill and still
   costs a compositing layer, which the frosted panels are already paying for.

   Colours are set explicitly rather than inherited: h3 takes its colour from
   the body (near-black, invisible here) and `p` is dimmed ink globally. */
.step {
  background: var(--ink); border: 1px solid var(--ink);
  border-radius: var(--r-md); padding: 20px;
  color: #FFFFFF;
}
/* The step numbers. On three flat black cards they are the only bit of play
   available, and at 13px grey they were a label nobody read. Big, tipped
   over, and filled with the house gradient — the same gradient the page sits
   on, so the numeral reads as the card being a window onto the ground behind
   it rather than as a stray splash of colour.

   Tipped from the left edge (transform-origin 0 50%) so the numeral pivots
   where it is anchored instead of drifting right, and the rotated box stays
   inside the card's padding.

   Gradient text has one failure mode worth guarding: it works by painting the
   background through the glyphs and making the text itself transparent, so a
   browser without background-clip renders three invisible numbers. Hence the
   @supports — the solid colour below is what you get if the clip is not
   available, not a value that is overridden unconditionally. */
.step b {
  display: block; width: fit-content;
  font-size: clamp(38px, 9vw, 46px); line-height: 1;
  font-variation-settings: "opsz" 40, "wght" 800;
  letter-spacing: -0.03em;
  color: rgba(255, 255, 255, 0.85);
  margin-bottom: 6px;
  transform: rotate(-6deg); transform-origin: 0 50%;
}
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .step b {
    background: var(--house);
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
  }
}
.step h3 { margin-bottom: 4px; color: #FFFFFF; }
.step p { font-size: 15px; color: rgba(255, 255, 255, 0.76); }

.points { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; margin-top: 14px; }
.point {
  background: var(--paper-solid); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 20px;
}
.point h3 { margin-bottom: 5px; }
.point p { font-size: 15px; }

/* The feature spots.

   The box is 120. The drawings do NOT fill it on their own: each was exported
   onto an 800x800 canvas with whatever margin its artboard happened to have,
   so the visible ink runs from 57% of the canvas (s02, s04) to 97% (s08). At
   one flat size that makes the shield look right and the other seven look
   shrunken, and enlarging the box enlarges the empty margin with it.

   So each spot carries a --k bringing its longest side to 86% of the box, and
   a --dx/--dy for the few drawn off-centre. Both were measured, not guessed:
   every frame of each animation rasterised and the visible-pixel bounds
   unioned across the cycle, so a shape that only appears mid-animation (s03's
   burst, which takes it from 57% to 87%) still fits. If a spot is redrawn,
   re-measure rather than nudging these by hand. */
.spot { width: 120px; height: 120px; margin-bottom: 12px; }
.spot img, .spot svg, .spot-play { display: block; width: 100%; height: 100%; }

/* Direct children only: the lottie <svg> sits inside .spot-play and would
   otherwise match both selectors and take the transform twice. Scale before
   translate, so the offsets stay in the box's own percentages instead of
   needing to be pre-multiplied by --k. */
.spot > img, .spot > svg, .spot > .spot-play {
  transform: scale(var(--k, 1)) translate(var(--dx, 0), var(--dy, 0));
  transform-origin: 50% 50%;
}
.spot[data-spot="s01-tap-to-stamp"]  { --k: 1.08; --dx: -8.7%; --dy: 8.5%; }
.spot[data-spot="s02-card-wallet"]   { --k: 1.25; --dy: -2.3%; }
.spot[data-spot="s03-rewards"]       { --k: 0.99; }
.spot[data-spot="s04-card-designer"] { --k: 1.13; --dy: -9.7%; }
.spot[data-spot="s05-nearby"]        { --k: 1.21; --dy: -1.7%; }
.spot[data-spot="s06-win-back"]      { --k: 1.09; --dx: 3%; --dy: -2.5%; }
.spot[data-spot="s07-stats"]         { --k: 1.32; }
.spot[data-spot="s08-secure"]        { --k: 0.88; }

/* The story band: a photograph with the words on top of it.

   The scrim is load-bearing, and it is sized for a picture nobody has chosen
   yet. Whatever ends up here will be brighter in places than the stand-in, so
   the gradient is heavier than it looks like it needs to be at the bottom,
   where the text sits, and thins toward the top where nothing is written. The
   numbers below were checked against the brightest pixels under the text of
   the committed stand-in, not against its average — an average would pass
   while the one white shopfront behind a word failed.

   No backdrop-filter and no blur: the picture is the point. If a future photo
   is bright enough to defeat this, the honest fix is a darker photo or a
   heavier scrim, not smaller text. */
.story {
  position: relative; overflow: hidden;
  border-radius: var(--r-lg);
  min-height: clamp(360px, 42vw, 460px);
  display: flex; align-items: flex-end;
  border: 1px solid var(--line);
  background: var(--ink);   /* the bed the narrow layout writes on */
}
.story > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The scrim sits between the photograph and the words, and getting that order
   wrong is silent: ::after is generated after the element's content, so with
   z-index auto it paints ON TOP of .story-text and dims the very text it
   exists to support — worst at the bottom, where the gradient is heaviest, so
   the last lines of the paragraph disappear while the heading merely greys.
   position:relative on the text is NOT enough to beat a later positioned
   sibling. Both layers are numbered explicitly so the order cannot drift. */
.story::after {
  content: ""; position: absolute; inset: 0; z-index: 0;
  /* Weighted to the left, not the bottom. The text occupies the left half and
     the photograph's subject — a shop sign — is upper right, so darkening the
     bottom evenly cost the picture the part worth showing and still left the
     ends of lines over a lit window display. Across to about 55%, which is
     where .story-text's 34em measure ends, then out of the way. The second,
     gentler gradient catches the last line where it overhangs. */
  background:
    linear-gradient(to right,
      rgba(8, 6, 5, 0.84) 0%,
      rgba(8, 6, 5, 0.80) 44%,
      rgba(8, 6, 5, 0.56) 58%,
      rgba(8, 6, 5, 0.22) 78%,
      rgba(8, 6, 5, 0.08) 100%),
    linear-gradient(to top,
      rgba(8, 6, 5, 0.34) 0%,
      rgba(8, 6, 5, 0.12) 45%,
      rgba(8, 6, 5, 0) 72%);
}
.story-text { position: relative; z-index: 1; padding: clamp(24px, 3.4vw, 40px); max-width: 34em; } /* border-box, so the 40px padding comes out of this: ~64 chars of text */
/* Narrow screens do not overlay at all. A 2.35:1 band inside a 390px column
   crops to roughly a sixth of its width — a shopfront becomes an unreadable
   slice of brick — and the text would need a scrim so heavy the photograph
   may as well not be there. So the picture keeps its own shape at the top and
   the words sit underneath on solid ink, which is the same contrast the three
   steps get. */
@media (max-width: 720px) {
  .story { display: block; min-height: 0; }
  .story > img { position: static; width: 100%; height: auto; aspect-ratio: 2.35 / 1; }
  .story::after { content: none; }
  .story-text { max-width: none; }
}

.story-text h2 { color: #FFFFFF; margin-bottom: 12px; }
.story-text p  { color: rgba(255, 255, 255, 0.88); }

.price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.price .fig {
  font-family: "Bricolage Grotesque", Archivo, sans-serif;
  font-variation-settings: "opsz" 40, "wght" 800;
  font-size: clamp(32px, 8vw, 46px); letter-spacing: -0.04em; line-height: 1;
}

footer {
  border-top: 1px solid var(--line); margin-top: clamp(44px, 9vw, 72px);
  padding-block: 26px 40px; color: var(--ink-dim); font-size: 14px;
}
footer p { margin: 0; }
footer p + p { margin-top: 4px; }
footer a { color: inherit; }

/* One scale for the whole policy, so a heading always sits closer to what it
   introduces than to what came before it, and a boxed panel is never flush
   against the text either side of it.

   `p + p` used to be the only gap rule in here, which meant everything it did
   not match fell to zero: an h3 touched its own paragraph AND the paragraph
   above it, and a panel butted straight into the text on both sides. */
/* A panel with something to look at beside the words.
   Text first in the source so a screen reader and a phone both get the point
   before the picture, which is decorative and carries an empty alt. */
.panel.split {
  display: grid; grid-template-columns: 1.05fr 0.95fr;
  gap: clamp(16px, 3vw, 30px); align-items: center;
}
/* Rounded like the card it sits in: the artwork is on white and the panel is a
   pale wash, so square corners read as a pasted rectangle rather than part of
   the thing. */
.panel.split img { width: 100%; height: auto; display: block; border-radius: 18px; }
@media (max-width: 760px) { .panel.split { grid-template-columns: 1fr; } }

/* The one heading on the page that is making a promise, so it gets to be the
   size of a promise rather than the size of a sub-head. */
.panel h3.big {
  font-family: "Bricolage Grotesque", Archivo, sans-serif;
  font-variation-settings: "opsz" 40, "wght" 800;
  font-size: clamp(26px, 4.2vw, 40px); line-height: 1.05;
  letter-spacing: -0.035em; margin-bottom: 10px;
}

/* A readable measure inside a wide page, for the sections that are prose while
   their neighbours are a grid or a photo band. Same 680px as .prose, and for
   the same reason: at 1080 a paragraph runs to about 150 characters a line. */
.narrow { max-width: 680px; margin-inline: auto; }
.narrow h2 { margin-top: 30px; margin-bottom: 8px; }
.narrow h3 { margin-top: 22px; margin-bottom: 6px; }
.narrow p, .narrow li { color: var(--ink-dim); }
.narrow p + p { margin-top: 10px; }
.narrow ul { margin: 10px 0 0; padding-left: 20px; }
.narrow li + li { margin-top: 6px; }

/* The legal pages, third time.
 *
 * Three things were wanted and only two of them fit any one layout, which is
 * why this has moved twice:
 *
 *   1. the words at a readable measure — 45-90 characters, not the 150 a
 *      paragraph runs to at the full 1080;
 *   2. the logo, the heading and the footer sharing one left edge;
 *   3. the page not looking like a ribbon down the middle of a wide screen.
 *
 * Holding the measure at 680 and centring it bought 1 and 3 and lost 2: the
 * nav sat wider than the words. Shrinking the whole wrap to hug the text
 * bought 1 and 2 and lost 3 — which is how the page ended up 800px of content
 * in the middle of a 1440px screen with the nav dragged in with it.
 *
 * All three fit if the frame and the measure stop being the same thing. The
 * wrap is the site's own 1080, so the nav and footer line up with every other
 * page; the words are 820 and start at the wrap's left edge rather than
 * centring in it, so they share that edge with the logo. What is left over is
 * a margin down the right-hand side, which on a page of legal prose reads as
 * room to breathe — and the nav button in the top right keeps the page
 * anchored on both sides rather than leaning.
 *
 * 820 rather than 760 because it is measured, not guessed: at this size it is
 * 74 characters a line, inside the comfortable band and the widest that band
 * allows. Set in px rather than ch because Bricolage's zero is far wider than
 * its average character, so a ch cap here reads about 40% looser than it
 * looks. */
/* No cap. The words use the same 1080 frame as everything else on the page.
 *
 * Three passes argued this the other way — a measure of 45-90 characters is
 * the textbook rule and I kept setting the column from it. On Neil's screen it
 * kept reading as a narrow strip with a heading and a panel that did not line
 * up with it, and he is the one looking at the real thing on the real monitor.
 * The rule is about sustained reading of running prose; this page is skimmed
 * for the bit somebody came for, and it is mostly headed sections and short
 * blocks rather than long paragraphs.
 *
 * What made the difference was one edge. The nav, the heading, the panel and
 * the words now all start and end in the same two places, which is the thing
 * that was actually wrong every time it was called bunched or slim. */
.prose h2 { margin-top: 30px; margin-bottom: 8px; }
.prose h3 { margin-top: 26px; margin-bottom: 8px; }
/* Tighter than a mid-section subheading, but two stacked headings still need
   air between them or they read as one block. */
.prose h2 + h3 { margin-top: 16px; }
.prose > section > :first-child { margin-top: 0; }
/* Type sized from the BODY, which is the mistake the last pass made: the
   measure was set by rendering the lead — 21px, 74 characters, fine — while
   the body underneath it is 17px and was running to 92 characters a line,
   past the comfortable 45-90 and worse than before the column was widened.
   Two sizes cannot share one column and both be right.
 *
 * So the column keeps its width and the type comes up to meet it. At 20px the
 * body is 77 characters across 820, and the lead at 24px is 66 — both in
 * band, and a legal page is one of the few places where larger type is simply
 * better. Measured at 1900px wide rather than reasoned about, because that is
 * how the 92 got missed.
 *
 * .small is excluded: the "last updated" line is a caption and reads as an
 * error at body size. */
.prose p:not(.small), .prose li { font-size: 20px; line-height: 1.55; }
.prose p, .prose li { color: var(--ink-dim); }

/* The hero lead runs the full measure here.
 *
 * `.wide .hero p` caps the lead at 30em so it does not run to 78 characters
 * across the marketing page's 1080. The legal pages joined .wide in the last
 * pass and inherited that cap — which put a 630px paragraph directly above an
 * 820px panel and left a step down the right-hand side. That step is what
 * read as bunched-up: five elements, three different right edges. */
.prose .hero p.lead { max-width: none; font-size: 24px; }
.prose p + p { margin-top: 10px; }
/* Enough to read as a separate object, less than the step to a new subheading. */
.prose .panel { margin-block: 16px; }
.prose :is(h2, h3) + .panel { margin-top: 8px; }

/* ——————————————————————————————————————————————————————————
   The drift: the stamp artwork, behind everything.

   Fixed to the window rather than the page, so it stays put while you scroll
   instead of scrolling a hundred stamps past you. Each one pops in with an
   overshoot, sits, shrinks away and comes round again — the same motion the
   app's sign-in screen uses.

   Three nested layers because each owns a different transform: drift moves,
   pop scales and fades, rock turns.
   —————————————————————————————————————————————————————————— */
.drift-layer {
  position: fixed; inset: 0; z-index: 0; overflow: hidden;
  pointer-events: none;
}
.drift { position: absolute; transform: translate(-50%, -50%);
  animation: floatY ease-in-out infinite alternate; }
.pop   { opacity: 0; animation-timing-function: linear;
  animation-iteration-count: infinite; animation-fill-mode: both; }
.rock  { animation: rock ease-in-out infinite alternate; }
.turn  { animation: turn linear infinite; }
.mote  { display: grid; place-items: center;
  background: radial-gradient(circle closest-side,
              rgba(255,255,255,.38) 0%, rgba(255,255,255,0) 72%); }
.mote img { display: block; }

@keyframes floatY {
  from { transform: translate(-50%,-50%) translateY(var(--rise)); }
  to   { transform: translate(-50%,-50%) translateY(calc(var(--rise) * -1)); }
}
@keyframes rock {
  from { transform: rotate(calc(var(--spin) * -1)); }
  to   { transform: rotate(var(--spin)); }
}
@keyframes turn { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

/* ——— narrow screens ——— */
@media (max-width: 720px) {
  .steps, .points { grid-template-columns: 1fr; }
  /* The content column fills a phone, so the drift has nowhere to be except
     underneath the words. Keep it faint enough to read through. */
  .drift-layer { opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .drift, .pop, .rock, .turn { animation: none; }
  .pop { opacity: calc(var(--peak) * 0.8); }
}

/* ——— FAQ ———————————————————————————————————————————————————————————————
   A list of questions that is also a list of answers, on pages whose job is
   an install or a sign-up.

   Native <details> rather than a scripted accordion. It opens before any
   JavaScript has run, the browser's own find-in-page reaches an open one, and
   a screen reader is told it is a disclosure without us having to say so —
   three things a div with a click handler gets wrong for free.

   Every one closed by default. An FAQ whose first answer is already open
   reads as an article with a strange opening paragraph, and the point of the
   list is that somebody can see all the questions at once and pick theirs. */
.faq { margin-top: 26px; }
.faq h2 { margin-bottom: 14px; }

.faq .q {
  background: var(--paper-solid); border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.faq .q + .q { margin-top: 8px; }

.faq summary {
  /* Both, because Safari answers to the pseudo-element and nothing else, and
     every other browser answers to list-style. */
  list-style: none;
  cursor: pointer;
  display: flex; align-items: flex-start; gap: 14px;
  padding: 15px clamp(16px, 3vw, 22px);
  font-weight: 650; line-height: 1.4;
}
.faq summary::-webkit-details-marker { display: none; }

/* A plus that becomes a minus: two gradient strokes rather than a glyph, so
   the mark is the same hairline weight as the border it sits inside and does
   not change with the font. The vertical stroke collapses to nothing on open,
   which is a movement the eye follows — swapping one character for another is
   not. */
.faq summary::after {
  content: "";
  flex: 0 0 13px; width: 13px; height: 13px;
  margin-left: auto; margin-top: 6px;
  background:
    linear-gradient(var(--ink-faint), var(--ink-faint)) center / 13px 1.5px no-repeat,
    linear-gradient(var(--ink-faint), var(--ink-faint)) center / 1.5px 13px no-repeat;
  transition: background-size 0.18s ease;
}
.faq .q[open] summary::after { background-size: 13px 1.5px, 1.5px 0; }

.faq summary:focus-visible {
  outline: 2px solid var(--ink); outline-offset: 2px;
  border-radius: var(--r-md);
}

.faq .a { padding: 0 clamp(16px, 3vw, 22px) 17px; }
.faq .a p { margin: 0; color: var(--ink-dim); font-size: 16px; }

/* An answer fills its row, and the row fills the page.

   `.wide .block p` caps prose at 32em, which is right for a paragraph that
   has the whole 1080 to itself and wrong here: the FAQ carries .block for its
   top margin, so every answer inherited a cap that stopped it two-thirds of
   the way across a row already drawn around it — which reads as a bug rather
   than as a measure. Three classes to its two, so this wins without !important
   and without touching the cap everything else on the page relies on. */
.wide .faq .a p { max-width: none; }
.faq .a p + p { margin-top: 10px; }
.faq .a a { color: inherit; }

@media (prefers-reduced-motion: reduce) {
  .faq summary::after { transition: none; }
}
