/* Sheaf — sheaf.sheesh.solutions
 *
 * The tokens below are transcribed from packages/sheaf_ui/lib/src/tokens/
 * sheaf_tokens.dart and docs/specs/motion.md. They are a COPY, not a second
 * source: the app is the source and this file follows it. web/tools/check.mjs
 * refuses a build when the two disagree on a colour.
 *
 * Three rules from the app hold here too, for the same reasons:
 *   - No blur anywhere. Layers separate by opaque fill and a 1px outline.
 *   - Scale and position may overshoot; opacity and colour never do.
 *   - Reduced motion is not silence: durations collapse, end states stay.
 */

/* ---------------------------------------------------------------- tokens */

:root {
  color-scheme: light dark;

  /* Brand */
  --primary: #4b3a8c;
  --primary-edge: #382a6b;
  --primary-bright: #6b57b8;
  --primary-container: #eae4f7;
  --on-primary: #ffffff;
  --on-primary-container: #4b3a8c;

  --accent: #f2a93b;
  --accent-edge: #ce8720;
  /* Gold is a fill, never a text colour: #F2A93B on white is 2.00:1. */
  --accent-ink: #8a5a0c;
  --on-accent: #2b2620;

  --flame: #f2762e;
  --success: #3e9e68;
  --success-ink: #2f7b50;
  --error: #c8553d;
  --error-ink: #a03f2c;

  /* Pillars — the same four in both themes */
  --pillar-songs: #7a5af8;
  --pillar-quiz: #f2a93b;
  --pillar-kids: #4fb3d9;
  --pillar-daily: #3e9e68;

  /* Surfaces */
  --bg: #fbf7f0;
  --surface: #ffffff;
  --surface-container: #f4ede2;
  --outline: #e3d8c7;
  --ink: #2b2620;
  --ink-muted: #6b6157;
  --neutral-200: #ede6da;
  --neutral-300: #d8cebe;

  --shadow-1: 0 2px 6px rgba(43, 38, 32, 0.08);
  --shadow-2: 0 8px 24px rgba(43, 38, 32, 0.14);

  /* Spacing — the app's 4px scale */
  --sp-xs: 4px;
  --sp-sm: 8px;
  --sp-md: 12px;
  --sp-base: 16px;
  --sp-lg: 20px;
  --sp-xl: 24px;
  --sp-2xl: 32px;
  --sp-3xl: 40px;
  --sp-huge: 56px;
  --sp-giant: 72px;

  /* Radii */
  --r-xs: 6px;
  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 24px;
  --r-xl: 32px;
  --r-pill: 999px;

  /* Motion */
  --d-fast: 150ms;
  --d-snappy: 200ms;
  --d-normal: 250ms;
  --d-relaxed: 300ms;
  --d-slow: 400ms;
  --ease-enter: cubic-bezier(0.05, 0.7, 0.1, 1);
  --ease-exit: cubic-bezier(0.3, 0, 0.8, 0.15);
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-pop: cubic-bezier(0.18, 0.9, 0.28, 1.35);

  --measure: 34rem;
  --page: 1120px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --primary: #9b87e0;
    /* In the dark the edge goes lighter, not darker — it is still the lit side. */
    --primary-edge: #b9a9ec;
    --primary-bright: #c0b0f0;
    --primary-container: #32294f;
    --on-primary: #16130f;
    --on-primary-container: #c0b0f0;

    --accent: #f5b85c;
    --accent-edge: #f8ce8c;
    --accent-ink: #f5b85c;
    --on-accent: #16130f;

    --flame: #f58b50;
    --success: #5cbe85;
    --success-ink: #5cbe85;
    --error: #e07a63;
    --error-ink: #e07a63;

    --bg: #16130f;
    --surface: #211d17;
    --surface-container: #2b261f;
    --outline: #3d362c;
    --ink: #f5efe4;
    --ink-muted: #a9a093;
    --neutral-200: #2b261f;
    --neutral-300: #3d362c;

    --shadow-1: 0 2px 6px rgba(0, 0, 0, 0.4);
    --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55);
  }
}

/* ----------------------------------------------------------------- fonts */

/* Self-hosted on purpose. A privacy page that fetches a font from a third
 * party contradicts the sentence it is printing. The .woff2 files are built
 * from the .ttf during the image build; local previews fall back to the .ttf. */
@font-face {
  font-family: 'Fraunces';
  src: url('../fonts/Fraunces-Variable.woff2') format('woff2-variations'),
       url('../fonts/Fraunces-Variable.ttf') format('truetype-variations');
  font-weight: 100 900;
  font-display: swap;
}

@font-face {
  font-family: 'Nunito';
  src: url('../fonts/Nunito-Variable.woff2') format('woff2-variations'),
       url('../fonts/Nunito-Variable.ttf') format('truetype-variations');
  font-weight: 200 1000;
  font-display: swap;
}

/* ------------------------------------------------------------------ base */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The header is sticky, so an anchor would otherwise land with its heading
   * underneath it. This is the whole fix and it belongs on the root. */
  scroll-padding-top: 84px;
  /* Nothing on this page may scroll the page sideways. Wide things scroll
   * inside their own box; see .prose table below. */
  overflow-x: hidden;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Nunito', ui-rounded, 'Segoe UI', system-ui, sans-serif;
  font-size: 17px;
  line-height: 1.53;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3 {
  font-family: 'Fraunces', ui-serif, Georgia, serif;
  font-weight: 600;
  line-height: 1.14;
  margin: 0;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

/* The floor is set by the narrowest phone still in use (320px), where
 * "A few minutes" has to fit on one line inside 280px of content width. */
h1 {
  font-size: clamp(2.1rem, 1.35rem + 3.4vw, 3.9rem);
}

h2 {
  font-size: clamp(1.75rem, 1.3rem + 1.8vw, 2.5rem);
}

h3 {
  font-family: 'Nunito', ui-rounded, system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.22rem;
  line-height: 1.28;
}

p {
  margin: 0;
  text-wrap: pretty;
}

a {
  color: inherit;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

:focus-visible {
  outline: 3px solid var(--primary);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}

.skip {
  position: absolute;
  left: -9999px;
  top: var(--sp-sm);
  background: var(--surface);
  color: var(--ink);
  padding: var(--sp-md) var(--sp-base);
  border-radius: var(--r-sm);
  border: 1px solid var(--outline);
  z-index: 100;
}

.skip:focus {
  left: var(--sp-base);
}

.wrap {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  /* env() so a notched phone in landscape does not put text under the sensor
   * housing. The max() keeps the ordinary case at 20px. */
  padding-inline: max(var(--sp-lg), env(safe-area-inset-left))
    max(var(--sp-lg), env(safe-area-inset-right));
}

@media (min-width: 640px) {
  .wrap {
    padding-inline: max(var(--sp-2xl), env(safe-area-inset-left))
      max(var(--sp-2xl), env(safe-area-inset-right));
  }
}

.eyebrow {
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: var(--sp-md);
}

.lede {
  font-size: clamp(1.06rem, 1rem + 0.4vw, 1.25rem);
  color: var(--ink-muted);
  max-width: var(--measure);
}

/* --------------------------------------------------------------- buttons */

/* The app's signature control: a face sitting on a darker edge, that travels
 * down onto the edge when pressed. box-shadow is the cheap web equivalent of
 * the two stacked boxes sheaf_ui draws. */
.btn {
  --edge: var(--primary-edge);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-md);
  min-height: 52px;
  padding: 0 var(--sp-xl);
  border: 0;
  border-radius: var(--r-md);
  background: var(--primary);
  color: var(--on-primary);
  font: inherit;
  font-weight: 800;
  font-size: 1rem;
  text-decoration: none;
  cursor: pointer;
  box-shadow: 0 4px 0 var(--edge);
  transform: translateY(0);
  transition: transform var(--d-fast) var(--ease-standard),
              box-shadow var(--d-fast) var(--ease-standard);
}

.btn:active {
  transform: translateY(3px);
  box-shadow: 0 1px 0 var(--edge);
}

.btn--accent {
  --edge: var(--accent-edge);
  background: var(--accent);
  color: var(--on-accent);
}

.btn--quiet {
  --edge: var(--outline);
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 4px 0 var(--edge), inset 0 0 0 1px var(--outline);
}

.btn--quiet:active {
  box-shadow: 0 1px 0 var(--edge), inset 0 0 0 1px var(--outline);
}

/* ---------------------------------------------------------------- header */

.site-head {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--bg);
  border-bottom: 1px solid transparent;
  transition: border-color var(--d-fast) var(--ease-standard);
}

.site-head[data-stuck='true'] {
  border-bottom-color: var(--outline);
}

.site-head__in {
  display: flex;
  align-items: center;
  gap: var(--sp-xl);
  min-height: 68px;
}

.brand {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  gap: var(--sp-md);
  text-decoration: none;
  font-family: 'Fraunces', ui-serif, Georgia, serif;
  font-weight: 600;
  font-size: 1.32rem;
  letter-spacing: -0.015em;
  margin-right: auto;
}

.brand img {
  width: 32px;
  height: 32px;
  border-radius: 8px;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-lg);
  font-size: 0.95rem;
  font-weight: 700;
}

.site-nav a {
  color: var(--ink-muted);
  text-decoration: none;
  /* 48px of height whatever the font size does — the app's touch-target floor,
   * and the reason this is padding rather than a line-height. */
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding-inline: var(--sp-xs);
  transition: color var(--d-fast) var(--ease-standard);
}

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

/* Below the fold-width the in-page anchors go and the one link that leaves the
 * page stays. No hamburger: a menu button that opens four anchors on a page
 * somebody is already scrolling is a control that costs a tap and saves none. */
@media (max-width: 780px) {
  .site-nav a[href^='/#'] {
    display: none;
  }
}

/* ------------------------------------------------------------------ hero */

.hero {
  padding-block: clamp(var(--sp-2xl), 5vw, var(--sp-giant)) var(--sp-huge);
}

.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(var(--sp-2xl), 5vw, var(--sp-giant));
  align-items: center;
}

.hero__copy > * + * {
  margin-top: var(--sp-xl);
}

.hero h1 em {
  font-style: normal;
  color: var(--primary);
}

.hero__note {
  font-size: 0.9rem;
  color: var(--ink-muted);
}

@media (max-width: 900px) {
  .hero__grid {
    grid-template-columns: 1fr;
    gap: var(--sp-2xl);
  }
}

/* ---------------------------------------------------------------- stores */

.stores {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-md);
}

/* On a phone the two rows go full width rather than sitting at their content
 * width. Two ragged buttons of different lengths read as a mistake; two rows
 * of the same width read as a choice. */
@media (max-width: 480px) {
  .stores {
    flex-direction: column;
    align-items: stretch;
  }
}

/* The app is in neither store yet. The row is deliberately not a link that
 * goes nowhere: it states the platform it detected and that it is not there
 * yet. `data-platform` on <html> puts the viewer's own platform first. */
.store {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-md);
  min-height: 60px;
  padding: 0 var(--sp-xl) 0 var(--sp-lg);
  border-radius: var(--r-md);
  border: 1px solid var(--outline);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  order: 2;
}

.store[data-first='true'] {
  order: 1;
  border-color: var(--primary);
  box-shadow: inset 0 0 0 1px var(--primary);
}

.store svg {
  width: 26px;
  height: 26px;
  flex: none;
  fill: currentColor;
}

.store__text {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}

.store__small {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.store__big {
  font-size: 1.02rem;
  font-weight: 800;
}

/* ----------------------------------------------------------------- phone */

/* Not a device frame (#476).
 *
 * It was one: 10px of bezel, a 44px radius and a shadow, wrapped round the
 * screenshot. That works when the picture has a phone's proportions, and these
 * do not — they are crops, roughly square, and a bezel round a square reads as
 * a small stubby device rather than as a phone. The owner's first word for it
 * was that it looked weird, and he was right.
 *
 * So it is a card holding a picture of a screen, which is what the crop
 * actually is. The screenshot carries its own status bar and its own rounded
 * top, so nothing is lost by not drawing a second frame round it — and there
 * is no aspect ratio to lie about. */
.phone {
  --w: min(300px, 78vw);
  width: var(--w);
  margin-inline: auto;
  border-radius: 22px;
  overflow: hidden;
  border: 1px solid var(--outline);
  box-shadow: var(--shadow-2);
}

.phone img {
  width: 100%;
  display: block;
}

/* Wide enough to hold its own against a 3.9rem headline. The tilt is gone with
 * the bezel: a rotated rectangle reads as a photograph of a screen, and it made
 * the fade at the bottom cut at an angle. */
.hero .phone {
  --w: min(372px, 84vw);
}

/* Smaller than the hero's on purpose: this one is evidence for a paragraph,
 * not the thing being introduced. */
.duo .phone {
  --w: min(282px, 72vw);
}

/* A screen shown down to the part being talked about, and cut rather than
 * shrunk — a whole phone at a third of the size is a picture of a phone, and
 * these are pictures of what is on one. The device ends where the crop does
 * instead of pretending to have a bottom edge.
 *
 * The cut is also the honest answer to a real problem: the catalogue holds one
 * song today, so the lower half of most screens is empty. Cropping shows less
 * of a true screenshot; inventing rows to fill it would show more of a false
 * one. */
/* The cut is a fade rather than an edge (#476).
 *
 * It used to end square: `padding-bottom: 0`, no bottom border, no bottom
 * radius. The intent was honest — the device ends where the crop does instead
 * of pretending to have a bottom edge — but a device frame with a straight
 * bottom line across it does not read as a deliberate crop. It reads as an
 * image that failed to load, and the first thing the owner said about this page
 * was that the screenshots looked broken.
 *
 * So the frame keeps its bottom bezel and its radius, and the screenshot
 * dissolves into it over the last stretch. A mask is the right tool rather than
 * a gradient overlay: an overlay has to know the colour behind it, and this
 * page has two of them. */
.phone--crop {
  overflow: hidden;
}

.phone--crop img {
  -webkit-mask-image: linear-gradient(to bottom, #000 88%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 88%, transparent 100%);
}

/* Safari before 15.4 and anything without mask-image gets the picture whole
 * rather than a hard cut — worse, but never broken-looking. */
@supports not (mask-image: linear-gradient(#000, transparent)) {
  .phone--crop img {
    border-bottom-left-radius: 34px;
    border-bottom-right-radius: 34px;
  }
}

/* --------------------------------------------------------------- section */

.section {
  padding-block: clamp(var(--sp-3xl), 6vw, var(--sp-giant));
}

.section--tint {
  background: var(--surface-container);
  border-block: 1px solid var(--outline);
}

.section__head {
  max-width: var(--measure);
  margin-bottom: var(--sp-3xl);
}

.section__head p {
  color: var(--ink-muted);
  margin-top: var(--sp-base);
}

/* --------------------------------------------------------------- pillars */

.pillars {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-lg);
}

@media (max-width: 1000px) {
  .pillars {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 600px) {
  .pillars {
    grid-template-columns: 1fr;
  }
}

.pillar {
  background: var(--surface);
  border: 1px solid var(--outline);
  border-radius: var(--r-lg);
  padding: var(--sp-xl);
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}

.pillar__mark {
  width: 40px;
  height: 40px;
  border-radius: var(--r-md);
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--tone) 16%, transparent);
  color: var(--tone);
  margin-bottom: var(--sp-sm);
}

.pillar__mark svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.pillar p {
  color: var(--ink-muted);
  font-size: 0.97rem;
}

/* ------------------------------------------------------------------ duo */

.duo {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(var(--sp-2xl), 5vw, var(--sp-giant));
  align-items: center;
}

.duo--flip .duo__media {
  order: -1;
}

/* One column, and the measure held (#476).
 *
 * The section lost its screenshot — it named a song, and no song title belongs
 * on this site. What is left must not become a half-empty grid with a paragraph
 * squeezed into 50% and nothing beside it, and it must not become a paragraph
 * running the full width of a desktop either: 45em is where prose stops being
 * comfortable to read. */
.duo--solo {
  grid-template-columns: minmax(0, 1fr);
}

.duo--solo .duo__copy {
  max-width: 45em;
}

@media (max-width: 900px) {
  .duo {
    grid-template-columns: 1fr;
  }

  .duo--flip .duo__media {
    order: 0;
  }
}

.duo__copy > * + * {
  margin-top: var(--sp-base);
}

/* ----------------------------------------------------------------- plans */

.plans {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-lg);
}

@media (max-width: 800px) {
  .plans {
    grid-template-columns: 1fr;
  }
}

.plan {
  background: var(--surface);
  border: 1px solid var(--outline);
  border-radius: var(--r-lg);
  padding: var(--sp-2xl);
  /* The two lists are different lengths and always will be. A column with the
   * footnote pushed to the bottom keeps the two cards reading as a pair rather
   * than as one card and one card with a hole in it. */
  display: flex;
  flex-direction: column;
}

.plan--plus {
  border-color: var(--primary);
  box-shadow: inset 0 0 0 1px var(--primary);
}

.plan h3 {
  font-size: 1.35rem;
}

.plan__sub {
  color: var(--ink-muted);
  margin-top: var(--sp-sm);
  font-size: 0.97rem;
}

.plan ul {
  margin-top: var(--sp-xl);
  display: flex;
  flex-direction: column;
  gap: var(--sp-base);
}

.plan li {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: var(--sp-md);
  align-items: start;
}

.plan li svg {
  width: 22px;
  height: 22px;
  margin-top: 2px;
  fill: none;
  stroke: var(--success-ink);
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.plan li b {
  display: block;
  font-weight: 800;
}

.plan li span {
  color: var(--ink-muted);
  font-size: 0.95rem;
}

.plan__foot {
  margin-top: auto;
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--outline);
  font-size: 0.9rem;
  color: var(--ink-muted);
}

/* ----------------------------------------------------------------- facts */

.facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-lg);
}

@media (max-width: 800px) {
  .facts {
    grid-template-columns: 1fr;
  }
}

.fact {
  padding: var(--sp-xl);
  border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--outline);
}

.fact h3 {
  margin-bottom: var(--sp-sm);
}

.fact p {
  color: var(--ink-muted);
  font-size: 0.97rem;
}

/* ------------------------------------------------------------------ note */

.note {
  border-left: 3px solid var(--accent);
  padding: var(--sp-sm) 0 var(--sp-sm) var(--sp-lg);
  color: var(--ink-muted);
  font-size: 0.97rem;
}

/* ------------------------------------------------------------------ prose */

.prose {
  max-width: 44rem;
  padding-block: var(--sp-huge);
}

.prose h1 {
  font-size: clamp(2rem, 1.5rem + 2vw, 2.75rem);
}

.prose h2 {
  font-size: 1.5rem;
  margin-top: var(--sp-huge);
}

.prose h3 {
  margin-top: var(--sp-2xl);
}

.prose p,
.prose ul {
  margin-top: var(--sp-base);
  color: var(--ink-muted);
}

.prose ul {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
  padding-left: var(--sp-lg);
  list-style: disc;
}

.prose li::marker {
  color: var(--neutral-300);
}

.prose a {
  color: var(--primary);
  text-underline-offset: 3px;
}

.prose__meta {
  margin-top: var(--sp-md);
  font-size: 0.9rem;
  color: var(--ink-muted);
}

.prose dl {
  margin: var(--sp-base) 0 0;
}

.prose dt {
  font-weight: 800;
  margin-top: var(--sp-base);
}

.prose dd {
  margin: var(--sp-xs) 0 0;
  color: var(--ink-muted);
}

/* ---------------------------------------------------------------- footer */

.site-foot {
  border-top: 1px solid var(--outline);
  padding-block: var(--sp-3xl);
  margin-top: var(--sp-2xl);
}

.site-foot__in {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-lg) var(--sp-2xl);
  align-items: center;
}

.site-foot nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-xs) var(--sp-base);
  margin-left: auto;
  font-size: 0.94rem;
  font-weight: 700;
}

.site-foot a {
  color: var(--ink-muted);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding-inline: var(--sp-sm);
}

.site-foot a:hover {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.site-foot__mark {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-md);
  font-size: 0.94rem;
  color: var(--ink-muted);
}

.site-foot__mark img {
  width: 28px;
  height: 28px;
  border-radius: 7px;
}

/* --------------------------------------------------------------- reveal */

/* Fade and 16px rise, the app's list-appear shape. Nothing here depends on
 * the animation for its meaning: no-JS and reduced-motion both land on the
 * same end state. */
.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity var(--d-slow) var(--ease-enter),
              transform var(--d-slow) var(--ease-enter);
}

.reveal[data-shown='true'],
.no-js .reveal {
  opacity: 1;
  transform: none;
}

/* The stagger, as nine rules rather than as an inline style (#476). Written out
 * rather than computed, because a stylesheet cannot multiply and an inline
 * style is what the CSP refuses. */
.reveal[data-stagger='1'] { transition-delay: 40ms; }
.reveal[data-stagger='2'] { transition-delay: 80ms; }
.reveal[data-stagger='3'] { transition-delay: 120ms; }
.reveal[data-stagger='4'] { transition-delay: 160ms; }
.reveal[data-stagger='5'] { transition-delay: 200ms; }
.reveal[data-stagger='6'] { transition-delay: 240ms; }
.reveal[data-stagger='7'] { transition-delay: 280ms; }
.reveal[data-stagger='8'] { transition-delay: 320ms; }

/* A page being printed, saved or captured is never scrolled through, so the
 * observer never fires and every unrevealed section comes out blank. The one
 * place a scroll animation can silently delete content is the one place it is
 * hardest to notice. */
@media print {
  .reveal {
    opacity: 1;
    transform: none;
  }

  .site-head,
  .phone {
    display: none;
  }

  body {
    background: #fff;
    color: #000;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  /* Not zero: an animation that never runs fires no completion callback, and
   * the end state is what has to survive, not the movement. */
  :root {
    --d-fast: 1ms;
    --d-snappy: 1ms;
    --d-normal: 1ms;
    --d-relaxed: 1ms;
    --d-slow: 1ms;
  }

  .reveal {
    transform: none;
  }

  .hero .phone {
    transform: none;
  }
}

/* ------------------------------------------------------- contact slots -- */

/* The email and the postal address arrive after an invisible challenge (#434).
 * They occupy their line from the first paint, so nothing below them moves
 * when the value lands — the same rule the app holds itself to, where a state
 * change may not shift the furniture around it. */
[data-contact] {
  min-height: 1.5em;
  display: inline-block;
}

[data-contact][data-state='loading'] {
  color: var(--neutral-400);
}

[data-contact][data-state='failed'] {
  color: var(--ink-muted);
  font-size: 0.95em;
}
