/* KCP landing page (ADR-0066). The page borrows the product's own structure: the
   weekly grid's quarter hour field behind the hero, the product's colour tokens, and
   its system Thai body stack. Orange is an accent only; the subject colours appear
   only where they mean a subject. */

@font-face {
  font-family: 'KCP Display';
  src: url('/assets/fonts/kcp-display.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* A local face tuned toward the display face's metrics, so the swap moves nothing. */
@font-face {
  font-family: 'KCP Display Fallback';
  src:
    local('Leelawadee UI Bold'), local('LeelawadeeUI-Bold'),
    local('Thonburi Bold'), local('Thonburi-Bold'),
    local('Noto Sans Thai Bold'), local('NotoSansThai-Bold');
  font-weight: 600;
  size-adjust: 104%;
  ascent-override: 107%;
  descent-override: 51%;
  line-gap-override: 0%;
}

:root {
  color-scheme: light;

  --site-ink: #1a1a1a;
  --site-ink-soft: #5c5c5c;
  --site-paper: #ffffff;
  --site-paper-alt: #f7f7f8;
  --site-rule: #e4e4e7;
  --site-rule-strong: #838389;
  --site-primary: #e35205;
  --site-primary-strong: #b8400a;
  --site-primary-hover: #a3380a;
  --site-primary-soft: #fdeee5;
  --site-chip: #e4e4e7;

  --grid-quarter: rgba(26, 26, 26, 0.05);
  --grid-hour: rgba(26, 26, 26, 0.09);
  --grid-noon: rgba(26, 26, 26, 0.16);

  --font-display:
    'KCP Display', 'KCP Display Fallback', 'Leelawadee UI', 'Thonburi',
    'Noto Sans Thai', system-ui, sans-serif;
  --font-body:
    'Segoe UI', 'Sukhumvit Set', 'Noto Sans Thai', 'Leelawadee UI', 'Thonburi',
    'Helvetica Neue', Arial, sans-serif;

  --measure-th: 34rem;
  --measure-en: 36rem;
  --gutter: 24px;
  --container: 1200px;
  --header-h: 64px;

  /* The page rhythm: one idea per block, separated by one section gap that grows a
     little with the window, with the space between a statement, its sub line and its
     visual on one scale. Only the pinned scenes take more room than this. */
  --space-section: 96px;
  --space-media: 40px;
  --space-sub: 16px;
  --space-cta: 32px;
  --margin-inline: 20px;

  --r-chip: 4px;
  --r-control: 8px;
  --r-frame: 12px;
  --r-pill: 999px;
  --shadow-frame: 0 4px 16px rgba(0, 0, 0, 0.1);

  --focus-color: var(--site-primary);
  --cta-h: 48px;
  --cta-h-large: 56px;

  --dur-fast: 150ms;
  --dur-slow: 300ms;
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-exit: cubic-bezier(0.4, 0, 1, 1);

  /* Motion: a long exponential settle for entrances and reveals, a symmetric ease for
     moves inside a stage. --k scales every travel distance; a phone travels less. */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --dur-reveal: 900ms;
  --dur-move: 700ms;
  --dur-fade: 400ms;
  --stagger: 80ms;
  --k: 1;
  --reveal-y: 48px;
  --ruler-stagger: 40ms;
}

@media (max-width: 639px) {
  :root {
    --k: 0.75;
    --reveal-y: 32px;
    --ruler-stagger: 30ms;
  }
}

@media (min-width: 640px) {
  :root {
    --space-section: clamp(96px, 12vh, 128px);
    --space-media: 56px;
    --space-sub: 20px;
    --space-cta: 40px;
    --margin-inline: 32px;
  }
}

@media (min-width: 1024px) {
  :root {
    --space-section: clamp(112px, 13vh, 144px);
    --space-media: 72px;
    --space-sub: 24px;
    --space-cta: 48px;
    --margin-inline: clamp(32px, 5vw, 64px);
  }
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 0ms;
    --dur-slow: 0ms;
    --dur-reveal: 0ms;
    --dur-move: 0ms;
    --dur-fade: 0ms;
    --stagger: 0ms;
  }
}

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

/* No smooth scrolling: a jump to a section or a focus move would otherwise run through
   several screens of pinned track, playing every scene in fast forward on the way. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-padding-top: calc(var(--header-h) + 16px);
}

body {
  margin: 0;
  background: var(--site-paper);
  color: var(--site-ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.75;
  font-weight: 400;
}

body:lang(en) {
  line-height: 1.6;
}

@media (min-width: 640px) {
  body {
    font-size: 17px;
  }
}

@media (min-width: 1024px) {
  body {
    font-size: 18px;
  }
}

h1,
h2,
h3,
h4,
p,
ol,
ul,
dl,
dd,
figure {
  margin: 0;
}

ul,
ol {
  padding: 0;
  list-style: none;
}

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

a {
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--focus-color);
  outline-offset: 2px;
}

main:focus {
  outline: none;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 600;
  text-wrap: balance;
}

h2 {
  font-size: 36px;
  line-height: 48px;
}

h3 {
  font-size: 24px;
  line-height: 36px;
}

h4 {
  font-size: 18px;
  line-height: 32px;
}

:lang(en) h2 {
  letter-spacing: -0.01em;
}

.body,
.faq__item p {
  text-wrap: pretty;
  max-width: var(--measure-th);
}

:lang(en) .body {
  max-width: var(--measure-en);
}

.body + .body {
  margin-top: 16px;
}

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

.container {
  width: min(var(--container), 100% - 2 * var(--margin-inline));
  margin-inline: auto;
}

.text-link {
  color: var(--site-primary-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.text-link:hover {
  color: var(--site-primary-hover);
}

.icon {
  flex: none;
}

/* Skip link and header */

.skip {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 20;
  padding: 8px 16px;
  border-radius: var(--r-control);
  background: var(--site-paper);
  color: var(--site-ink);
  font-weight: 600;
  translate: 0 -200%;
}

.skip:focus {
  translate: none;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  height: var(--header-h);
  background: var(--site-paper);
  border-bottom: 1px solid var(--site-rule);
}

.site-header__inner {
  display: flex;
  height: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.brand {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}

.brand__word {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.site-nav ul {
  display: flex;
  align-items: center;
  gap: 4px;
}

.site-nav a {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  padding-inline: 10px;
  border-radius: var(--r-control);
  font-size: 16px;
  text-decoration: none;
}

.site-nav a:hover {
  text-decoration: underline;
  text-underline-offset: 4px;
}

.site-nav .nav-install {
  min-height: 40px;
  margin-block: 2px;
  margin-left: 8px;
  padding-inline: 16px;
  border: 1px solid var(--site-rule-strong);
  font-family: var(--font-display);
  font-weight: 600;
}

.site-nav__wide {
  display: none;
}

/* Below 1024 px the header keeps one section link, the FAQ, under a short name, so the
   answers about phones and the web version are one tap away. */
@media (min-width: 1024px) {
  .site-nav__wide {
    display: list-item;
  }

  .site-nav__narrow {
    display: none;
  }
}

/* The narrowest phones tighten the header so its four links fit on one line, and the
   story grid gives its day columns the room for a subject id. */
@media (max-width: 359px) {
  .story {
    --story-label: 34px;
  }

  .site-nav ul {
    gap: 0;
  }

  .site-nav a {
    padding-inline: 6px;
    white-space: nowrap;
  }

  .site-nav .nav-install {
    margin-left: 4px;
    padding-inline: 10px;
  }
}

/* A full size touch target for the header install link on a touch screen. */
@media (pointer: coarse) {
  .site-nav .nav-install {
    min-height: 44px;
    margin-block: 0;
  }
}

/* Hero */

/* The hero is kept short so the real product starts inside the first screen of a
   laptop: a two line lede, one line of fine print, and tight gaps around them. */
.hero {
  position: relative;
  padding-top: 40px;
  overflow-x: clip;
}

.field {
  position: absolute;
  inset: 0 0 auto;
  width: min(var(--container), 100% - 2 * var(--margin-inline));
  height: 420px;
  margin-inline: auto;
  background-image:
    linear-gradient(
      to right,
      transparent calc(40% - 1px),
      var(--grid-noon) calc(40% - 1px) 40%,
      transparent 40%
    ),
    repeating-linear-gradient(
      to right,
      var(--grid-hour) 0 1px,
      transparent 1px 10%
    ),
    repeating-linear-gradient(
      to right,
      var(--grid-quarter) 0 1px,
      transparent 1px 2.5%
    );
  /* Full strength in the ruler band, then faint behind the headline and the lede, so
     no hairline cuts through the text. */
  -webkit-mask-image: linear-gradient(
    to bottom,
    #000 0 40px,
    rgba(0, 0, 0, 0.4) 40px,
    transparent 100%
  );
  mask-image: linear-gradient(
    to bottom,
    #000 0 40px,
    rgba(0, 0, 0, 0.4) 40px,
    transparent 100%
  );
  pointer-events: none;
}

.field__ruler {
  position: relative;
  height: 40px;
  border-bottom: 1px solid var(--grid-hour);
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 600;
  line-height: 22px;
  font-variant-numeric: tabular-nums;
  color: var(--site-ink-soft);
}

.field__ruler span {
  position: absolute;
  top: 9px;
  padding-left: 6px;
}

/* The day opens and closes on a stronger rule than the hours between. */
.field__ruler::before,
.field__ruler::after {
  content: '';
  position: absolute;
  top: 0;
  width: 1px;
  height: 40px;
  background: var(--grid-noon);
}

.field__ruler::before {
  left: 0;
}

.field__ruler::after {
  right: 0;
}

.field__ruler span:nth-child(1) {
  --n: 0;
  left: 0;
}
.field__ruler span:nth-child(2) {
  --n: 1;
  left: 10%;
}
.field__ruler span:nth-child(3) {
  --n: 2;
  left: 20%;
}
.field__ruler span:nth-child(4) {
  --n: 3;
  left: 30%;
}
.field__ruler span:nth-child(5) {
  --n: 4;
  left: 40%;
  color: var(--site-primary-strong);
}
.field__ruler span:nth-child(6) {
  --n: 5;
  left: 50%;
}
.field__ruler span:nth-child(7) {
  --n: 6;
  left: 60%;
}
.field__ruler span:nth-child(8) {
  --n: 7;
  left: 70%;
}
.field__ruler span:nth-child(9) {
  --n: 8;
  left: 80%;
}
.field__ruler span:nth-child(10) {
  --n: 9;
  left: 90%;
}
/* The closing hour sits inside the right edge rather than past it. */
.field__ruler span:nth-child(11) {
  --n: 10;
  right: 0;
  padding-right: 6px;
  padding-left: 0;
}

/* Below 1040 px every other hour is hidden so the labels never touch; noon and the
   closing hour stay. Below 640 px the closing hour would meet 16:00, so the end rule
   alone marks it there. */
@media (max-width: 1039px) {
  .field__ruler span:nth-child(even) {
    display: none;
  }
}

@media (max-width: 639px) {
  .field__ruler span:nth-child(11) {
    display: none;
  }
}

.field__ruler span:nth-child(5)::before {
  content: '';
  position: absolute;
  top: -9px;
  left: -1px;
  width: 3px;
  height: 40px;
  background: var(--site-primary);
}

.hero__inner {
  position: relative;
}

/* The lockup stands clear of the ruler band above it. */
.hero__text {
  max-width: 1040px;
  padding-top: 32px;
}

@media (min-width: 640px) {
  .hero__text {
    padding-top: 40px;
  }
}

.hero h1 {
  display: block;
}

.hero__lockup {
  display: block;
  margin-bottom: 12px;
  font-size: 16px;
  line-height: 1.5;
  color: var(--site-ink-soft);
}

.hero__display {
  display: block;
  font-size: 40px;
  line-height: 1.25;
  text-wrap: balance;
}

:lang(en) .hero__display {
  line-height: 1.08;
  letter-spacing: -0.02em;
}

.lede {
  max-width: 32em;
  text-wrap: pretty;
  margin-top: var(--space-sub);
  font-size: 18px;
  line-height: 1.65;
  color: var(--site-ink-soft);
}

.hero .cta-row {
  margin-top: var(--space-cta);
}

.micro {
  margin-top: 8px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--site-ink-soft);
}

.cta-row + .micro {
  margin-top: 16px;
}

@media (min-width: 640px) {
  .hero__lockup {
    font-size: 18px;
  }

  .hero__display {
    font-size: 60px;
  }

  .lede {
    font-size: 20px;
  }

  .micro {
    font-size: 14px;
  }
}

@media (min-width: 1024px) {
  .hero__display {
    font-size: 80px;
  }

  :lang(en) .hero__display {
    font-size: 84px;
  }

  .lede {
    font-size: 22px;
  }
}

/* The product frame. Its size is the same in every state: at most the container, and
   on a short window no taller than the window under the header with a margin, at the
   1440 by 900 ratio of the capture plus its 40 px bar. */
.hero__pin {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  padding-top: var(--space-media);
}

.frame-rise {
  width: min(
    var(--container),
    100% - 2 * var(--margin-inline),
    (100svh - var(--header-h) - 48px - 40px) * 1.6
  );
}

.frame {
  position: relative;
  border: 1px solid var(--site-rule);
  border-radius: var(--r-frame);
  background: var(--site-paper);
  box-shadow: var(--shadow-frame);
}

.frame__bar {
  display: flex;
  align-items: center;
  gap: 16px;
  height: 40px;
  padding-inline: 14px;
  border-bottom: 1px solid var(--site-rule);
  border-radius: calc(var(--r-frame) - 1px) calc(var(--r-frame) - 1px) 0 0;
  background: var(--site-paper-alt);
}

.frame__dots {
  display: flex;
  flex: none;
  gap: 8px;
}

.frame__dots i {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #d4d4d8;
}

.frame__url {
  display: flex;
  min-width: 0;
  max-width: 560px;
  flex: 1;
  align-items: center;
  gap: 6px;
  height: 26px;
  padding-inline: 10px;
  border: 1px solid var(--site-rule);
  border-radius: var(--r-control);
  background: var(--site-paper);
  color: var(--site-ink-soft);
  font-size: 13px;
  line-height: 18px;
}

.frame__url span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.frame__img {
  width: 100%;
  border-radius: 0 0 calc(var(--r-frame) - 1px) calc(var(--r-frame) - 1px);
}

/* Calls to action: the two store badges as one pair at one height, the touch note on a
   coarse pointer, then the web version on a line of its own as a smaller, quieter
   control, so the badges read as the primary pair. A badge is never changed beyond
   resizing in proportion, so its height is set and its width follows; on a narrow phone
   the height shrinks until both fit one row. The row is measured against its own width
   less a few pixels, since a scrollbar or a width rounded up by a pixel fraction would
   otherwise push the second badge onto a row of its own. */

.cta-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  margin-top: 24px;
  container-type: inline-size;
}

.stores {
  --badge-gap: 12px;
  /* The two badges are 6.1733 times as wide as they are tall, side by side. */
  --badge-h: min(
    52px,
    (100vw - 2 * var(--margin-inline) - var(--badge-gap) - 24px) / 6.1733
  );

  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--badge-gap);
}

@supports (width: 1cqi) {
  .stores {
    --badge-h: min(52px, (100cqi - var(--badge-gap) - 4px) / 6.1733);
  }
}

.store {
  display: block;
  flex: none;
  border-radius: 6px;
}

.store:focus-visible {
  outline-offset: 4px;
}

.store__badge {
  width: auto;
  max-width: none;
  height: var(--badge-h);
}

/* The web version reads as a control that is not open yet: a small outlined pill with a
   dashed edge and its coming soon label, under the badges. It stays disabled to
   assistive technology and takes no hover state. */
.soon {
  display: inline-flex;
  height: 40px;
  align-items: center;
  gap: 6px;
  padding: 0 6px 0 12px;
  border: 1px dashed var(--site-rule-strong);
  border-radius: var(--r-pill);
  background: transparent;
  color: var(--site-ink-soft);
  font: inherit;
  font-size: 14px;
  line-height: 20px;
  cursor: default;
}

.soon .icon {
  width: 16px;
  height: 16px;
}

.soon .badge {
  margin-left: 2px;
  font-size: 13px;
  background: var(--site-chip);
  color: var(--site-ink-soft);
}

.badge {
  display: inline-block;
  margin-left: 6px;
  padding: 0 8px;
  border-radius: var(--r-pill);
  background: var(--site-chip);
  color: var(--site-ink);
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 400;
  line-height: 20px;
  vertical-align: 1px;
}

.cta-coarse {
  display: none;
  margin-top: -8px;
  font-size: 14px;
  line-height: 20px;
  color: var(--site-ink-soft);
}

/* A phone cannot install from the stores, so the note says where the badges lead. */
@media (pointer: coarse) and (max-width: 1023px) {
  .cta-coarse {
    display: block;
  }
}

@media (min-width: 640px) {
  .stores {
    --badge-gap: 16px;
    --badge-h: 56px;
  }
}

@media (forced-colors: active) {
  .soon {
    border: 1px solid GrayText;
    color: GrayText;
  }

  :focus-visible {
    outline-color: Highlight;
  }
}

/* Statements and screens. Every block of the page is one statement: a headline and
   one line under it, with at most one visual, so no two ideas share a screen. */

.screen {
  padding-block: var(--space-section);
}

.screen + .screen,
.problem + .screen {
  padding-top: 0;
}

.statement__h {
  max-width: 14em;
  font-size: 32px;
  line-height: 1.3;
}

.statement__h:lang(en) {
  max-width: 16em;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.statement__line {
  display: block;
}

/* Words that read as one unit stay on one line. */
.keep {
  white-space: nowrap;
}

.statement__sub {
  margin-top: var(--space-sub);
  max-width: 30rem;
  font-size: 18px;
  line-height: 1.65;
  color: var(--site-ink-soft);
  text-wrap: pretty;
}

.statement__sub:lang(en) {
  max-width: 28rem;
}

@media (min-width: 640px) {
  .statement__h {
    font-size: 48px;
  }

  .statement__sub {
    font-size: 20px;
  }
}

@media (min-width: 1024px) {
  /* A split screen gives its statement five of twelve columns, so the display size
     reaches its full 64 px only where those columns are wide enough. */
  .statement__h {
    font-size: clamp(48px, 4.5vw, 64px);
  }

  .statement__sub {
    font-size: 22px;
  }
}

/* Dedupe stage. The rows are grids of a fixed track set, so a row never changes the
   width of a column, and the merge moves rows by transform alone in a table that keeps
   its height. */

.dedupe-track {
  margin-top: var(--space-media);
}

.dedupe-stage {
  --row-h: 44px;

  display: grid;
  grid-template-areas:
    'count'
    'table';
  gap: 16px;
  padding: 24px;
  border-radius: 16px;
  background: var(--site-paper-alt);
}

/* The foot of the card the merge raises exists only for the motion. */
.dedupe-card {
  display: none;
}

.count {
  grid-area: count;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 600;
  line-height: 1.2;
}

/* The number is the headline of the stage, its unit a quiet word beside it. */
.count__num {
  font-size: 56px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.count__join {
  color: var(--site-ink-soft);
}

.count__sections {
  color: var(--site-primary-strong);
}

/* Each count keeps its number with its unit. */
.count__rows,
.count__sections {
  white-space: nowrap;
}

/* The rows lay out by the width the table gets, not the window: the stage puts the table
   in a full width row on a tablet and in eight of twelve columns on a desktop, so a
   narrow table can sit in a wide window. */
.dedupe-table {
  position: relative;
  grid-area: table;
  min-width: 0;
  container: dedupe / inline-size;
}

.dedupe {
  display: flex;
  flex-direction: column;
  width: 100%;
  font-size: 14px;
  line-height: 18px;
}

.dedupe thead,
.dedupe tbody {
  display: block;
}

/* The caption is the source line, set as micro text under the table. */
.dedupe caption {
  order: 1;
  padding-top: 12px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--site-ink-soft);
  text-align: left;
}

.dedupe tr {
  position: relative;
  display: grid;
  min-height: var(--row-h);
  align-items: center;
}

.dedupe tbody tr {
  border-bottom: 1px solid var(--site-rule);
  background: var(--site-paper);
  box-shadow: inset 4px 0 0 var(--subject);
}

.dedupe th,
.dedupe td {
  display: block;
  min-width: 0;
  padding: 0 10px;
}

.dedupe th {
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 600;
  text-align: left;
}

.dedupe__id,
.dedupe__time,
.dedupe__seats {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.dedupe__grouping {
  display: grid;
  justify-items: start;
}

.dedupe__chip {
  max-width: 100%;
  margin-top: 2px;
  overflow: hidden;
  padding: 0 8px;
  border-radius: var(--r-chip);
  background: var(--site-primary-soft);
  color: var(--site-primary-strong);
  font-size: 12px;
  line-height: 20px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dedupe__result {
  margin-top: 16px;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 600;
  line-height: 1.4;
  text-wrap: balance;
}

/* At rest the table shows its merged state: the four sections, tinted, each with the
   chip that counts its listings, under the counter that names both numbers. The repeat
   rows stay in the markup for the motion and the data. */
.dedupe tbody tr[data-row='repeat'] {
  display: none;
}

:root:not(.motion) .dedupe tbody tr[data-row='first'] {
  background: var(--site-primary-soft);
}

/* A table narrower than its six columns draws each row as two short lines: the subject
   id, name and section, then the curriculum and the day and time. The seats stay in the
   markup for the data. Six columns need 720 px, so the curriculum keeps room for its
   name and for the chip that replaces it. */
@container dedupe (max-width: 719.98px) {
  .dedupe thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .dedupe tbody tr {
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
      'id name section'
      'group group time';
    column-gap: 8px;
    padding: 4px 12px 4px 16px;
  }

  .dedupe td {
    padding: 0;
  }

  .dedupe__id,
  .dedupe__name,
  .dedupe__section {
    font-size: 13px;
    font-weight: 600;
  }

  .dedupe__id {
    grid-area: id;
  }

  .dedupe__name {
    grid-area: name;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .dedupe__section {
    grid-area: section;
    white-space: nowrap;
  }

  .dedupe__grouping {
    grid-area: group;
  }

  .dedupe__grouping,
  .dedupe__time {
    color: var(--site-ink-soft);
    font-size: 12px;
  }

  .dedupe__time {
    grid-area: time;
  }

  .dedupe .dedupe__seats {
    display: none;
  }
}

@media (min-width: 640px) {
  .dedupe-stage {
    row-gap: 24px;
  }

  .count {
    font-size: 22px;
  }

  .count__num {
    font-size: 80px;
  }
}

@container dedupe (min-width: 720px) {
  .dedupe tr {
    grid-template-columns: minmax(0, 1fr) 84px 150px 96px 116px 64px;
  }

  .dedupe:lang(en) tr {
    grid-template-columns: minmax(0, 1fr) 84px 196px 100px 116px 64px;
  }

  .dedupe thead tr {
    border-bottom: 1px solid var(--site-ink);
  }

  .dedupe__name,
  .dedupe__section {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

@media (min-width: 1024px) {
  .dedupe-stage {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    grid-template-areas: none;
    column-gap: var(--gutter);
    align-items: start;
    padding: 32px;
  }

  .count {
    grid-area: 1 / 1 / 2 / span 4;
  }

  .dedupe-table {
    grid-area: 1 / 5 / 2 / -1;
  }

  .count {
    font-size: 24px;
  }

  .count__num {
    font-size: 96px;
  }

  .count > span {
    display: block;
  }
}

/* Each subject colour, and its tint over white at the product's block alpha. */
.subject-0 {
  --subject: #1f5fa8;
  --tint: #dde7f2;
}
.subject-1 {
  --subject: #0e7c7b;
  --tint: #dbebeb;
}
.subject-2 {
  --subject: #2e7d32;
  --tint: #e0ece0;
}
.subject-3 {
  --subject: #6a3fa0;
  --tint: #e9e2f1;
}
.subject-4 {
  --subject: #b0355f;
  --tint: #f3e1e7;
}
.subject-5 {
  --subject: #3949ab;
  --tint: #e1e4f2;
}
.subject-6 {
  --subject: #a21caf;
  --tint: #f1ddf3;
}
.subject-7 {
  --subject: #8d5a2b;
  --tint: #eee6df;
}
.subject-8 {
  --subject: #5d4037;
  --tint: #e7e2e1;
}
.subject-9 {
  --subject: #455a64;
  --tint: #e3e6e8;
}

.plate .plate__img,
.poster img {
  width: 100%;
  border: 1px solid var(--site-rule);
  border-radius: var(--r-frame);
  background: var(--site-paper);
}

.screen__grid {
  display: grid;
  gap: var(--space-media);
}

/* A plate fills its column up to the size it reads best at; justified to the end of the
   grid, it still needs that width, or it shrinks around its percentage image. */
.exam__plate {
  width: 100%;
  max-width: 420px;
}

.plans__plate {
  width: 100%;
  max-width: 680px;
}

@media (min-width: 1024px) {
  .screen__grid {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--gutter);
    align-items: center;
  }

  .screen__grid > .statement {
    grid-column: 1 / span 5;
  }

  .exam .screen__grid > .plate {
    grid-column: 8 / -1;
    justify-self: end;
  }

  .plans .screen__grid > .plate {
    grid-column: 6 / -1;
    justify-self: end;
  }

  .privacy .screen__grid > .statement {
    grid-column: 1 / span 6;
  }

  .privacy__perms {
    grid-column: 8 / -1;
  }
}

/* Export posters, the one overlapping composition on the page */

.posters {
  position: relative;
  margin-top: var(--space-media);
}

/* On a phone the wallpaper stands below the wide poster, overlapping its foot. */
.poster--phone {
  position: relative;
  width: 56%;
  margin: -48px auto 0;
}

.poster--phone img {
  border: 2px solid var(--site-paper);
  outline: 1px solid var(--site-rule);
  box-shadow: var(--shadow-frame);
}

/* The landscape poster runs the full width, and the phone wallpaper stands over its
   empty late afternoon columns, clear of the header and of the footer credit, bleeding
   a little past the right edge so the overlap reads as placed. */
@media (min-width: 640px) {
  .poster--phone {
    position: absolute;
    top: 12%;
    right: max(-4%, 8px - var(--margin-inline));
    width: 20%;
    margin: 0;
  }
}

/* Privacy */

.perms {
  margin-top: 8px;
}

.privacy__perms {
  margin-top: var(--space-media);
}

.privacy__perms h3 {
  font-size: 18px;
  line-height: 1.5;
  color: var(--site-ink-soft);
}

@media (min-width: 1024px) {
  .privacy__perms {
    margin-top: 0;
  }
}

.perm {
  padding-block: 16px;
  border-top: 1px solid var(--site-rule);
}

.perm dt {
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.75;
}

.perm dd {
  max-width: var(--measure-th);
  color: var(--site-ink);
}

.perms + p {
  margin-top: 16px;
}

/* Story: the one dark section. The timetable is built in HTML from plan H, with the
   product's own orientation: days as rows and time across from 640 px, days as columns
   and time down on a phone, as on the phone wallpaper (ADR-0043). It enters and leaves
   on a hard edge; its colour never animates. */

.story {
  --ink-bg: #111113;
  --ink-text: #f5f5f7;
  --ink-soft: #a1a1a6;
  --ink-rule: rgba(255, 255, 255, 0.1);
  --ink-hour: rgba(255, 255, 255, 0.16);
  --ink-edge: rgba(255, 255, 255, 0.28);
  --focus-color: #ff8a4c;
  --story-label: 48px;
  --story-axis: 28px;
  --story-row: 88px;
  --story-readout: 32px;

  padding-block: var(--space-section);
  overflow-x: clip;
  background: var(--ink-bg);
  color: var(--ink-text);
}

.story__pin {
  display: grid;
  gap: var(--space-media);
  align-items: center;
}

.story__h {
  font-size: 16px;
  line-height: 1.5;
  color: var(--ink-soft);
}

.story__beats {
  display: grid;
  gap: 16px;
  margin-top: var(--space-sub);
  counter-reset: beat;
}

.story__beat {
  font-family: var(--font-display);
  font-size: 28px;
  font-weight: 600;
  line-height: 1.3;
  text-wrap: balance;
  counter-increment: beat;
}

.story__beat:lang(en) {
  line-height: 1.15;
  letter-spacing: -0.015em;
}

.story__beat::before {
  content: counter(beat, decimal-leading-zero);
  display: block;
  margin-bottom: 4px;
  font-size: 14px;
  line-height: 1.5;
  color: #ff8a4c;
  font-variant-numeric: tabular-nums;
}

/* The grid: a label track, then forty quarter hour tracks, so a block starts and ends
   on the quarter the product would place it. */
.story__grid {
  position: relative;
  display: grid;
  grid-template-columns: var(--story-label) repeat(40, minmax(0, 1fr));
  grid-template-rows: var(--story-axis) repeat(5, var(--story-row)) var(
      --story-readout
    );
  font-size: 13px;
  line-height: 1.4;
}

.story__rules {
  grid-area: 2 / 2 / 7 / -1;
  border-inline: 1px solid var(--ink-edge);
  background-image:
    linear-gradient(
      to right,
      transparent calc(40% - 1px),
      var(--ink-edge) calc(40% - 1px) 40%,
      transparent 40%
    ),
    repeating-linear-gradient(
      to right,
      var(--ink-hour) 0 1px,
      transparent 1px 10%
    ),
    repeating-linear-gradient(
      to bottom,
      var(--ink-rule) 0 1px,
      transparent 1px 20%
    );
}

.story__axis {
  grid-area: 1 / 2 / 2 / -1;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--ink-soft);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* Each hour label is a zero width item, so it centres on its gridline. */
.story__axis > span {
  display: flex;
  width: 0;
  justify-content: center;
  white-space: nowrap;
}

.story__day {
  grid-column: 1;
  align-self: center;
  color: var(--ink-soft);
  font-family: var(--font-display);
  font-weight: 600;
}

.story__block,
.story__ghost {
  position: relative;
  padding: 3px 2px;
}

.story__card {
  position: relative;
  display: flex;
  height: 100%;
  flex-direction: column;
  padding: 6px 8px 6px 12px;
  border-radius: 6px;
  background: var(--tint);
  box-shadow: inset 4px 0 0 var(--subject);
  color: #1a1a1a;
}

/* Every block leads with its subject id, the label a phone keeps when it drops the name. */
.story__id {
  padding-right: 28px;
  color: #5c5c5c;
  font-size: 12px;
  line-height: 18px;
  font-variant-numeric: tabular-nums;
}

.story__name {
  display: -webkit-box;
  overflow: hidden;
  font-weight: 600;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

.story__sec {
  position: absolute;
  top: 6px;
  right: 6px;
  min-width: 20px;
  padding: 0 4px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.7);
  font-size: 11px;
  line-height: 18px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.story__time {
  margin-top: auto;
  color: #5c5c5c;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* The section dragged in and its clash chip exist only for the motion; at rest the grid
   shows the finished plan. */
.story__ghost,
.story__chip {
  display: none;
}

.story__readout {
  grid-row: -2;
  grid-column: 1 / -1;
  align-self: end;
  color: var(--ink-soft);
  font-size: 13px;
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
}

.d-1 {
  grid-row: 2;
}
.d-2 {
  grid-row: 3;
}
.d-3 {
  grid-row: 4;
}
.d-4 {
  grid-row: 5;
}
.d-5 {
  grid-row: 6;
}
.h-8 {
  grid-column-start: 2;
}
.h-9 {
  grid-column-start: 6;
}
.h-10 {
  grid-column-start: 10;
}
.h-11 {
  grid-column-start: 14;
}
.h-12 {
  grid-column-start: 18;
}
.h-13 {
  grid-column-start: 22;
}
.h-14 {
  grid-column-start: 26;
}
.h-15 {
  grid-column-start: 30;
}
.h-16 {
  grid-column-start: 34;
}
.h-17 {
  grid-column-start: 38;
}
.span-3 {
  grid-column-end: span 12;
}

@media (min-width: 640px) and (max-width: 1023px) {
  .story {
    --story-row: 72px;
  }

  .story__name {
    -webkit-line-clamp: 1;
    line-clamp: 1;
  }
}

@media (min-width: 640px) {
  .story__h {
    font-size: 18px;
  }

  .story__beat {
    font-size: 36px;
  }
}

@media (min-width: 1024px) {
  .story__pin {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--gutter);
  }

  .story__text {
    grid-column: 1 / span 4;
  }

  .story__grid {
    grid-column: 5 / -1;
  }

  .story__h {
    font-size: 20px;
  }

  .story__beat {
    font-size: 48px;
  }
}

/* A phone transposes the grid: days are five columns and time runs down in quarter hour
   tracks, so every block stays wide enough to name its subject. */
@media (max-width: 639px) {
  .story {
    --story-label: 40px;
  }

  .story__grid {
    grid-template-columns: var(--story-label) repeat(5, minmax(0, 1fr));
    grid-template-rows: 28px repeat(40, var(--q, 9px)) 28px;
    font-size: 11px;
  }

  .story__rules {
    grid-area: 2 / 2 / -2 / -1;
    border-inline: 0;
    border-block: 1px solid var(--ink-edge);
    background-image:
      linear-gradient(
        to bottom,
        transparent calc(40% - 1px),
        var(--ink-edge) calc(40% - 1px) 40%,
        transparent 40%
      ),
      repeating-linear-gradient(
        to bottom,
        var(--ink-hour) 0 1px,
        transparent 1px 10%
      ),
      repeating-linear-gradient(
        to right,
        var(--ink-rule) 0 1px,
        transparent 1px 20%
      );
  }

  .story__axis {
    grid-area: 2 / 1 / -2 / 2;
    flex-direction: column;
    align-items: start;
    font-size: 11px;
  }

  .story__axis > span {
    width: auto;
    height: 0;
    align-items: center;
  }

  .story__grid .story__day {
    grid-row: 1;
    justify-self: center;
  }

  .story__card {
    padding: 8px 3px 4px 5px;
    box-shadow: inset 0 4px 0 var(--subject);
  }

  /* A day column is too narrow for a subject name in either language, which would break
     inside a Thai word or a long English one, so a phone labels every block by its
     subject id, and an id never breaks. */
  .story__name {
    display: none;
  }

  .story__id {
    overflow: hidden;
    padding-right: 0;
    color: #1a1a1a;
    font-size: min(10px, 2.6vw);
    line-height: 1.4;
    letter-spacing: -0.01em;
    font-weight: 600;
    white-space: nowrap;
  }

  .story__sec {
    top: auto;
    right: 3px;
    bottom: 3px;
    min-width: 16px;
    font-size: 10px;
    line-height: 16px;
  }

  .story__time {
    display: none;
  }

  .d-1 {
    grid-row: auto;
    grid-column: 2;
  }
  .d-2 {
    grid-row: auto;
    grid-column: 3;
  }
  .d-3 {
    grid-row: auto;
    grid-column: 4;
  }
  .d-4 {
    grid-row: auto;
    grid-column: 5;
  }
  .d-5 {
    grid-row: auto;
    grid-column: 6;
  }
  .h-8 {
    grid-row-start: 2;
  }
  .h-9 {
    grid-row-start: 6;
  }
  .h-10 {
    grid-row-start: 10;
  }
  .h-11 {
    grid-row-start: 14;
  }
  .h-12 {
    grid-row-start: 18;
  }
  .h-13 {
    grid-row-start: 22;
  }
  .h-14 {
    grid-row-start: 26;
  }
  .h-15 {
    grid-row-start: 30;
  }
  .h-16 {
    grid-row-start: 34;
  }
  .h-17 {
    grid-row-start: 38;
  }
  .span-3 {
    grid-row-end: span 12;
  }
}

/* And more: four tiles on one baseline, each led by its glyph on a soft orange square, so
   the row reads as a set of objects rather than a column of captions. The tiles stagger
   only as they enter. */

.facts {
  display: grid;
  gap: 16px;
  margin-top: var(--space-media);
}

.fact {
  padding: 24px 24px 28px;
  border-radius: 16px;
  background: var(--site-paper-alt);
}

.fact .icon {
  display: block;
  box-sizing: content-box;
  width: 28px;
  height: 28px;
  padding: 14px;
  border-radius: 14px;
  background: var(--site-primary-soft);
  color: var(--site-primary-strong);
}

.fact dt {
  margin-top: 24px;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 600;
  line-height: 1.4;
}

@media (min-width: 1024px) {
  .fact dt {
    font-size: 22px;
  }
}

.fact dd {
  color: var(--site-ink-soft);
}

/* A phone sets each tile as a row, its glyph beside the term, so the four facts take
   about half a screen rather than more than one. */
@media (max-width: 639px) {
  .fact {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 16px;
    align-items: center;
    padding: 20px;
  }

  .fact .icon {
    grid-row: 1 / span 2;
    align-self: start;
    width: 24px;
    height: 24px;
    padding: 12px;
    border-radius: 12px;
  }

  .fact dt {
    margin-top: 0;
    font-size: 18px;
  }

  .fact dd {
    font-size: 15px;
    line-height: 1.6;
  }
}

@media (min-width: 640px) {
  .facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
    gap: var(--gutter);
  }

  .fact {
    padding: 28px 28px 32px;
  }
}

@media (min-width: 1024px) {
  .facts {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }

  .fact {
    min-height: 260px;
  }
}

/* Download band */

.download {
  padding-block: var(--space-section);
  background: var(--site-paper-alt);
}

.download__inner {
  display: grid;
  justify-items: center;
  text-align: center;
}

/* The band's cluster sits optically centred: a mark sized under the headline, the badges
   a call to action gap under it, the meta line a step under them, and as much room under
   the cluster as over it. */
.download__mark {
  width: 72px;
  height: 72px;
  margin-bottom: 32px;
}

.download .statement__h {
  max-width: none;
}

.download .cta-row {
  margin-top: var(--space-cta);
}

@media (min-width: 1024px) {
  .download__mark {
    width: 88px;
    height: 88px;
    margin-bottom: 40px;
  }
}

.download .cta-row {
  justify-self: stretch;
  align-items: center;
}

.download .stores {
  justify-content: center;
}

.download .micro {
  margin-top: 20px;
}

/* FAQ */

.faq__list {
  max-width: 820px;
  margin-top: var(--space-media);
  border-bottom: 1px solid var(--site-rule);
  content-visibility: auto;
  contain-intrinsic-size: auto 760px;
}

.faq__item {
  border-top: 1px solid var(--site-rule);
}

.faq__item summary {
  display: flex;
  min-height: 44px;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-block: 16px;
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 600;
  line-height: 32px;
  cursor: pointer;
  list-style: none;
  border-radius: var(--r-control);
}

.faq__item summary::-webkit-details-marker {
  display: none;
}

.faq__chevron {
  color: var(--site-ink-soft);
}

@media (prefers-reduced-motion: no-preference) {
  .faq__chevron {
    transition: transform var(--dur-fast) var(--ease-standard);
  }
}

.faq__item[open] .faq__chevron {
  transform: rotate(180deg);
}

.faq__item p {
  max-width: 40rem;
  padding-bottom: 24px;
}

/* Footer */

.site-footer {
  padding-block: 48px 64px;
  border-top: 1px solid var(--site-rule);
  font-size: 14px;
  line-height: 22px;
  color: var(--site-ink-soft);
  content-visibility: auto;
  contain-intrinsic-size: auto 360px;
}

.site-footer__grid {
  display: grid;
  gap: 24px;
}

.site-footer__text p + p {
  margin-top: 12px;
}

.site-footer__links a {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  color: var(--site-ink);
}

.site-footer__legal {
  font-size: 12px;
  line-height: 18px;
}

@media (min-width: 1024px) {
  .site-footer__grid {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--gutter);
  }

  .site-footer__text {
    grid-column: 1 / span 8;
    max-width: 44rem;
  }

  .site-footer nav {
    grid-column: 10 / span 3;
  }

  .site-footer__legal {
    grid-column: 1 / -1;
  }

  .site-footer__links a {
    min-height: 36px;
  }
}

/* Not found */

.notfound {
  padding-block: 96px;
}

.notfound__block {
  margin-top: 32px;
}

.notfound__block .body {
  margin-top: 8px;
}

/* The leading block's h1 takes the h2 scale, so both language blocks share one rhythm
   and a Thai heading never sits in a line box shorter than its marks. */
.notfound__block h1 {
  font-size: 36px;
  line-height: 48px;
}

@media (max-width: 639px) {
  .notfound__block h1 {
    font-size: 30px;
    line-height: 42px;
  }
}

/* Narrow screens */

/* A Thai compound the line breaker would split stays whole at every width. */
.nw {
  white-space: nowrap;
}

/* A Thai headline phrase stays whole wherever it fits on one line. English phrases only
   group words for the entrance, so they wrap at any space. */
@media (min-width: 360px) {
  .phrase:lang(th) {
    white-space: nowrap;
  }
}

/* Under 360 px the hero and the statements set their display a step smaller, so each Thai
   phrase still fits whole on its line, an English phrase kept together fits too, and no
   short word is left alone. */
@media (max-width: 359px) {
  .hero__display:lang(th) {
    font-size: 34px;
  }

  .statement__h {
    font-size: 30px;
  }

  .hero__display .phrase:lang(th),
  .statement__h .phrase:lang(th) {
    white-space: nowrap;
  }
}

/* Under 390 px the English headline sets its size from the window width, so its second
   phrase, before you register, stays on one line instead of leaving its last word alone
   on a line of its own. */
@media (max-width: 389px) {
  .hero__display:lang(en) {
    font-size: clamp(30px, (100vw - 40px) / 8.9, 40px);
  }

  .hero__break .phrase:lang(en) {
    white-space: nowrap;
  }
}

@media (max-width: 1023px) {
  .hero__break {
    display: inline;
  }

  /* Thai sets no space between the two phrases, so a narrow line would split the second
     one; it keeps a line of its own instead. */
  .hero__break:lang(th) {
    display: block;
  }
}

@media (min-width: 1024px) {
  .hero__break {
    display: block;
  }
}

@media (max-width: 639px) {
  :root {
    --header-h: 56px;
  }

  .hero {
    padding-top: 32px;
  }

  .frame__bar {
    gap: 10px;
    padding-inline: 10px;
  }

  .site-nav a {
    padding-inline: 8px;
  }
}

/* Motion (ADR-0066, revision 2). site.js sets .motion on the root before first layout,
   unless the reader prefers reduced motion; without it, or without the script, every
   block above is its final state and nothing below applies. Where the browser runs
   scroll driven animations the reveals, the hero frame and the closing band follow the
   scroll (path A); elsewhere .no-sda marks path B, where site.js adds classes as blocks
   come into view and transitions play them. The two pinned scenes step on their own
   clock in both paths. Only transform and opacity ever animate; the merged rows' tint
   swaps at once. */

/* The hero entrance plays once on load, on a timeline in both paths. */

.motion .field__ruler span {
  animation: rise 500ms var(--ease-out-expo) both;
  animation-delay: calc(var(--n) * var(--ruler-stagger));
  --rise: 6px;
}

.motion .field__ruler span:nth-child(5)::before {
  transform-origin: top;
  animation: grow-y 600ms var(--ease-out-expo) 200ms both;
}

.motion .hero__lockup {
  animation: rise 700ms var(--ease-out-expo) 100ms both;
  --rise: 12px;
}

/* The headline rises phrase by phrase, never letter by letter, so no Thai mark is ever
   split from its base. */
.motion .hero h1 .phrase {
  display: inline-block;
  animation: rise 900ms var(--ease-out-expo) both;
  animation-delay: calc(80ms + 100ms * var(--p, 0));
  --rise: 0.35em;
}

.motion .hero__display > .phrase:nth-of-type(2) {
  --p: 1;
}

.motion .hero__break .phrase {
  --p: 2;
}

.motion :lang(en) .hero__break .phrase {
  --p: 1;
}

.motion .hero .lede {
  animation: rise 700ms var(--ease-out-expo) 200ms both;
  --rise: 16px;
}

/* The install row is the page's action, so it shows at once: it is close to full
   strength within 200 ms of load, while the headline is still rising. */
.motion .hero .cta-row {
  animation: rise 500ms var(--ease-out-expo) both;
  --rise: 10px;
}

.motion .hero__fine {
  animation: fade 400ms linear 300ms both;
}

/* The frame only lifts; its opacity never changes, so the largest paint is not held
   back. */
.motion .frame-rise {
  animation: lift 1100ms var(--ease-out-expo) 300ms both;
  --rise: 40px;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(calc(var(--rise) * var(--k)));
  }
}

@keyframes lift {
  from {
    transform: translateY(calc(var(--rise) * var(--k)));
  }
}

@keyframes fade {
  from {
    opacity: 0;
  }
}

@keyframes grow-y {
  from {
    transform: scaleY(0);
  }
}

/* Section reveals: one per block, never per paragraph. The block fades in step with the
   scroll and only its rise settles on the long exponential curve, so a steady scroll
   never sees the text jump most of the way to full strength in one frame. */

@keyframes reveal-fade {
  from {
    opacity: 0;
  }
}

@keyframes reveal-rise {
  from {
    transform: translateY(var(--reveal-y));
    animation-timing-function: var(--ease-out-expo);
  }

  to {
    transform: none;
  }
}

@supports (animation-timeline: view()) {
  .motion:not(.no-sda) [data-reveal] {
    animation:
      reveal-fade linear both,
      reveal-rise linear both;
    animation-timeline: view();
    /* A tall block finishes by the time its top is 85 percent down the window, so text
       at rest is rarely short of full strength; a short one still takes at least 96 px of
       scroll, so it never snaps in. */
    animation-range: entry min(10%, 2svh) entry max(min(70%, 15svh), 96px);
  }

  .motion:not(.no-sda) .statement__sub[data-reveal] {
    animation-range: entry 20% entry max(80%, 20% + 96px);
  }

  .motion:not(.no-sda) .fact[data-reveal] {
    animation-range: entry calc(10% + 6% * var(--i)) entry
      calc(60% + 6% * var(--i));
  }
}

.motion.no-sda [data-reveal] {
  transition:
    opacity var(--dur-reveal) var(--ease-out-expo),
    transform var(--dur-reveal) var(--ease-out-expo);
}

.motion.no-sda [data-reveal]:not(.is-in) {
  opacity: 0;
  transform: translateY(var(--reveal-y));
}

.motion.no-sda .statement__sub[data-reveal] {
  transition-delay: 120ms;
}

.motion.no-sda .fact[data-reveal] {
  transition-delay: calc(80ms * var(--i));
}

/* The hero frame on scroll. On a window tall enough to hold it, the frame stands in a
   pinned stage: it starts tilted back like a laptop lid under the buttons and stands up
   to full size over three quarters of a screen of scroll, while the hero text steps
   back, then holds standing for a while before the page moves on. Elsewhere it scales
   in as it enters the view. */

@media (min-width: 768px) and (min-height: 680px) {
  /* The tilted frame sits lower than its box, so the stage starts a little higher to
     keep the lid close under the buttons. */
  .motion .hero__track {
    height: calc(100svh - var(--header-h) + 100svh);
    margin-top: -64px;
  }

  /* The stage box is taller than the frame it centres and starts above it, over the
     install buttons, so only the frame itself takes the pointer. */
  .motion .hero__pin {
    position: sticky;
    top: var(--header-h);
    height: calc(100svh - var(--header-h));
    align-items: center;
    padding-top: 0;
    pointer-events: none;
  }

  .motion .frame {
    isolation: isolate;
    transform-origin: 50% 100%;
    pointer-events: auto;
  }

  .motion.no-sda .frame {
    transition: transform 1500ms var(--ease-out-expo);
  }

  .motion.no-sda .hero__track:not(.is-in) .frame {
    transform: perspective(2000px) rotateX(16deg) scale(0.86);
  }

  .motion.no-sda .hero__fine > span {
    transition: opacity var(--dur-fade) linear;
  }

  .motion.no-sda .hero:has(.hero__track.is-in) .hero__fine > span {
    opacity: 0;
  }
}

@media (max-width: 767px), (max-height: 679px) {
  .motion.no-sda .frame {
    transition: transform 1500ms var(--ease-out-expo);
  }

  .motion.no-sda .hero__track:not(.is-in) .frame {
    transform: scale(0.92) translateY(24px);
  }
}

@supports (animation-timeline: view()) {
  @media (min-width: 768px) and (min-height: 680px) {
    .motion:not(.no-sda) .frame {
      animation: hero-zoom linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 75svh;
    }

    /* The deep shadow is drawn once and only its opacity changes. */
    .motion:not(.no-sda) .frame::after {
      content: '';
      position: absolute;
      inset: -1px;
      z-index: -1;
      border-radius: inherit;
      box-shadow: 0 40px 80px rgba(0, 0, 0, 0.18);
      animation: fade linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 75svh;
    }

    .motion:not(.no-sda) .hero__text {
      animation: hero-recede linear both;
      animation-timeline: scroll(root block);
      animation-range: 10svh 60svh;
    }

    /* The fine print sits where the standing frame's top edge passes, so it is gone
       before the frame reaches it. */
    .motion:not(.no-sda) .hero__fine > span {
      animation: fade linear reverse both;
      animation-timeline: scroll(root block);
      animation-range: 0 7svh;
    }
  }

  @media (max-width: 767px), (max-height: 679px) {
    .motion:not(.no-sda) .frame {
      animation: frame-in linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 35%;
    }
  }
}

@keyframes hero-zoom {
  from {
    transform: perspective(2000px) rotateX(16deg) scale(0.86);
    animation-timing-function: cubic-bezier(0.2, 0.7, 0.2, 1);
  }

  to {
    transform: none;
  }
}

@keyframes hero-recede {
  to {
    opacity: 0.15;
    transform: translateY(-32px);
  }
}

@keyframes frame-in {
  from {
    transform: scale(0.92) translateY(24px);
    animation-timing-function: var(--ease-out-expo);
  }

  to {
    transform: none;
  }
}

/* The dedupe merge plays in two steps, so no two texts ever share a place at partial
   opacity and the counter always names what the table shows. Step one: the repeat rows
   fade where they stand while the counter turns from rows to sections. Step two: the four
   sections close up into their slots while the card shrinks to them, and each label swap
   empties its box before the next text enters it. Under motion every row has a fixed
   height, so a section row moves by translateY((slot - index) * row height) and the
   table keeps its height. The card shrinks by transforms alone: its head has the merged
   size, its body folds up into the head, and its rounded foot rises with the caption by
   the height the repeats leave. site.js sets data-step as the scroll crosses each line,
   one step at a time, and each step plays on its own clock with transitions, so a fast
   scroll still sees every step whole. */

/* Under motion the table starts with all of its rows. */
.motion .dedupe tbody tr[data-row='repeat'] {
  display: grid;
}

/* One header height above its point, where site.js watches it cross the window's top: a
   share of the pinned scroll, --at, and past the release a share of a window, --past. */
.sentinel {
  position: absolute;
  top: calc(
    var(--at) * (100% - 100svh + var(--header-h)) + var(--past, 0) *
      (100svh - var(--header-h)) - var(--header-h)
  );
  left: 0;
  width: 1px;
  height: 0;
  pointer-events: none;
}

.motion .dedupe-track {
  position: relative;
}

.motion .dedupe-stage {
  --head-h: 0px;
  --result-space: 40px;
  --recenter: 0;
  /* How far everything under the table rises once merged: the rows the repeats held,
     less the room the result line takes under the sections. site.js sets --gone and
     --kept from the counts the page is built from. */
  --close: max(0px, var(--gone, 0) * var(--row-h) - var(--result-space));

  position: relative;
  isolation: isolate;
  background: none;
}

.motion .dedupe-stage::before,
.motion .dedupe-stage::after {
  content: '';
  position: absolute;
  right: 0;
  left: 0;
  z-index: -1;
  background: var(--site-paper-alt);
}

/* The head of the card: the size of the merged stage, rounded on every corner. */
.motion .dedupe-stage::before {
  top: 0;
  bottom: var(--close);
  border-radius: 16px;
}

/* The body: the height the repeats leave, folding up into the head as they go. */
.motion .dedupe-stage::after {
  bottom: 16px;
  height: var(--close);
  transform-origin: top;
}

/* The foot: the card's rounded lower edge, rising with the caption. */
.motion .dedupe-card {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: -1;
  display: block;
  height: 16px;
  border-radius: 0 0 16px 16px;
  background: var(--site-paper-alt);
}

.motion .dedupe thead tr {
  height: var(--row-h);
}

.motion .dedupe tbody tr {
  --shift: calc((var(--t) - var(--i)) * var(--row-h));

  z-index: 1;
  height: var(--row-h);
  min-height: 0;
}

.motion .dedupe tbody tr[data-row='first'] {
  z-index: 2;
}

/* The curriculum and the chip that replaces it share one cell, one fading out before
   the other fades in. */
.motion .dedupe__grouping {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

.motion .dedupe__group,
.motion .dedupe__chip {
  grid-area: 1 / 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.motion .dedupe__group {
  justify-self: stretch;
}

.motion .dedupe__chip {
  justify-self: start;
  margin-top: 0;
  opacity: 0;
}

/* Both counts share one clipped cell; the rows slide out before the sections slide in. */
.motion .count {
  display: grid;
  justify-items: start;
  overflow: clip;
  padding-block: 0.15em;
  margin-block: -0.15em;
}

.motion .count > span {
  grid-area: 1 / 1;
}

.motion .count__join {
  display: none;
}

.motion .count__sections {
  opacity: 0;
  transform: translateY(calc(100% + 0.2em));
}

/* The result takes the place under the sections, which the caption rises to meet. */
/* It starts on the table's left edge, as the caption under it does. */
.motion .dedupe__result {
  position: absolute;
  top: calc(var(--head-h) + var(--kept, 4) * var(--row-h) + 14px);
  right: 0;
  left: 0;
  margin: 0;
  opacity: 0;
  transform: translateY(12px);
}

@container dedupe (min-width: 720px) {
  .motion .dedupe,
  .motion .dedupe__result {
    --head-h: var(--row-h);
  }

  .motion .dedupe__grouping {
    align-self: center;
  }
}

/* Back towards the thirteen rows each step reverses quickly; the transitions written on
   the step states play it forward. */
.motion [data-step] tbody tr[data-row='repeat'] {
  transition:
    opacity 250ms linear,
    transform 250ms var(--ease-out-expo);
}

.motion [data-step] tbody tr[data-row='first'] {
  transition: transform 350ms var(--ease-in-out);
}

.motion [data-step] .dedupe caption,
.motion [data-step] .dedupe-card,
.motion [data-step] .dedupe-stage,
.motion [data-step] .dedupe-stage::after {
  transition: transform 350ms var(--ease-in-out);
}

.motion [data-step] .count__rows {
  transition:
    opacity 200ms linear 100ms,
    transform 300ms var(--ease-out-expo) 100ms;
}

.motion [data-step] .count__sections {
  transition:
    opacity 150ms linear,
    transform 200ms var(--ease-in-out);
}

.motion [data-step] tr[data-row='first'] .dedupe__group {
  transition: opacity 150ms linear 100ms;
}

.motion [data-step] tr[data-row='first'] .dedupe__chip {
  transition: opacity 100ms linear;
}

.motion [data-step] .dedupe__result {
  transition:
    opacity 150ms linear,
    transform 150ms linear;
}

/* Every forward step scales its transitions by --pace, which site.js sets on the stepped
   element: 1 for a reader who stops to read, less for one who is scrolling on. */
.motion [data-step] {
  --pace: 1;
}

/* Step one, about 800 ms: the repeats fade one after another while the rows count slides
   out of its box and the sections count slides in behind it, so the number changes with
   the rows it counts. */
.motion [data-step]:not([data-step='0']) tbody tr[data-row='repeat'] {
  opacity: 0;
  transform: scale(0.96);
  transition:
    opacity calc(450ms * var(--pace)) linear calc(var(--i) * 25ms * var(--pace)),
    transform calc(450ms * var(--pace)) var(--ease-out-expo)
      calc(var(--i) * 25ms * var(--pace));
}

.motion [data-step]:not([data-step='0']) .count__rows {
  opacity: 0;
  transform: translateY(calc(-100% - 0.2em));
  transition:
    opacity calc(300ms * var(--pace)) linear calc(150ms * var(--pace)),
    transform calc(450ms * var(--pace)) var(--ease-in-out)
      calc(150ms * var(--pace));
}

.motion [data-step]:not([data-step='0']) .count__sections {
  opacity: 1;
  transform: none;
  transition:
    opacity calc(300ms * var(--pace)) linear calc(350ms * var(--pace)),
    transform calc(450ms * var(--pace)) var(--ease-out-expo)
      calc(350ms * var(--pace));
}

/* Step two, about 1000 ms: the sections close up with the card, then trade their
   curriculum for the chip that counts their listings while the result rises. */
.motion [data-step='2'] tbody tr[data-row='first'] {
  background: var(--site-primary-soft);
  transform: translateY(var(--shift));
  transition:
    transform calc(var(--dur-move) * var(--pace)) var(--ease-in-out),
    background-color 0s linear calc(300ms * var(--pace));
}

.motion [data-step='2'] .dedupe caption,
.motion [data-step='2'] .dedupe-card {
  transform: translateY(calc(-1 * var(--close)));
}

.motion [data-step='2'] .dedupe-stage {
  transform: translateY(calc(var(--close) / 2 * var(--recenter)));
}

.motion [data-step='2'] .dedupe-stage::after {
  transform: scaleY(0);
}

.motion [data-step='2'] .dedupe caption,
.motion [data-step='2'] .dedupe-card,
.motion [data-step='2'] .dedupe-stage,
.motion [data-step='2'] .dedupe-stage::after {
  transition: transform calc(var(--dur-move) * var(--pace)) var(--ease-in-out);
}

.motion [data-step='2'] tr[data-row='first'] .dedupe__group {
  opacity: 0;
  transition: opacity calc(250ms * var(--pace)) linear calc(400ms * var(--pace));
}

.motion [data-step='2'] tr[data-row='first'] .dedupe__chip {
  opacity: 1;
  transition: opacity calc(250ms * var(--pace)) linear calc(700ms * var(--pace));
}

.motion [data-step='2'] .dedupe__result {
  opacity: 1;
  transform: none;
  transition:
    opacity calc(350ms * var(--pace)) linear calc(550ms * var(--pace)),
    transform calc(500ms * var(--pace)) var(--ease-out-expo)
      calc(550ms * var(--pace));
}

/* The pinned stage: on any window at least 540 px tall it holds still while the reader
   scrolls the length of its track and stays centred in the window as the card shrinks.
   A shorter window gets denser rows rather than an unpinned table, so the whole merge
   stays on one screen. The track ends soon after the merged state has held, so the pin
   lets go once the merge has been read. Its pinned scroll, --pinned, is one and a half
   screens and at least 1300 px, so on a short phone, where 150svh is under 1000 px, a
   reader scrolling at a steady pace still sees the merged card whole for most of a second
   before the pin lets go; the merge starts as the card rises the last stretch to the
   header, which lets the floor stay that short. */
@media (min-height: 540px) {
  .motion .dedupe-track {
    --pinned: max(150svh, 1300px);

    height: calc(100svh - var(--header-h) + var(--pinned));
  }

  .motion .dedupe-pin {
    position: sticky;
    top: var(--header-h);
    display: grid;
    height: calc(100svh - var(--header-h));
    align-content: center;
  }

  .motion .dedupe-stage {
    --recenter: 1;
  }

  /* The centred card leaves half the height it gave up under it, more than a section
     gap, so the dark band rises over the end of the pinned track and the page turns dark
     while the reader is still moving. */
  .motion .problem {
    padding-bottom: 0;
  }

  .motion .problem + .story {
    position: relative;
    margin-top: calc(85px - 24.5svh);
  }
}

/* A tall phone: the count above two lines a row. */
@media (max-width: 639px) and (min-height: 760px) {
  .motion .dedupe-stage {
    gap: 12px;
    padding: 16px 12px;
  }
}

/* A shorter phone sets a smaller count over denser rows. */
@media (max-width: 639px) and (min-height: 540px) and (max-height: 759px) {
  .motion .dedupe-stage {
    gap: 10px;
    padding: 12px;
  }

  .motion .count {
    font-size: 18px;
  }

  .motion .count__num {
    font-size: 40px;
  }
}

@media (min-width: 640px) and (min-height: 540px) and (max-height: 819px) {
  .motion .dedupe-stage {
    padding-block: 20px;
  }

  .motion .count__num {
    font-size: 64px;
  }
}

@media (min-width: 1024px) and (min-height: 820px) {
  .motion .dedupe-stage {
    padding-block: 24px;
  }
}

/* Two denser row heights: 34 px keeps two short lines a row where the table is narrow,
   and 26 px keeps one line, the subject id, the section and the time. */
@media (max-width: 639px) and (min-height: 540px) and (max-height: 759px),
  (min-width: 640px) and (min-height: 540px) and (max-height: 819px) {
  .motion .dedupe-stage {
    --row-h: 34px;
    --result-space: 36px;
  }

  .motion .dedupe__result {
    font-size: 16px;
  }

  @container dedupe (max-width: 719.98px) {
    .motion .dedupe tbody tr {
      padding-block: 1px;
    }

    .motion .dedupe__id,
    .motion .dedupe__name,
    .motion .dedupe__section {
      font-size: 12px;
      line-height: 16px;
    }

    .motion .dedupe__grouping,
    .motion .dedupe__time,
    .motion .dedupe__chip {
      font-size: 11px;
      line-height: 15px;
    }
  }
}

@media (max-width: 639px) and (min-height: 540px) and (max-height: 619px),
  (min-width: 640px) and (min-height: 540px) and (max-height: 659px) {
  .motion .dedupe-stage {
    --row-h: 26px;
  }

  .motion .dedupe {
    font-size: 13px;
  }

  @container dedupe (max-width: 719.98px) {
    .motion .dedupe tbody tr {
      grid-template-columns: auto minmax(0, 1fr) auto;
      grid-template-areas: 'id section time';
      padding-block: 0;
    }

    .motion .dedupe .dedupe__name,
    .motion .dedupe .dedupe__grouping {
      display: none;
    }

    .motion .dedupe__id,
    .motion .dedupe__section,
    .motion .dedupe__time {
      font-size: 12px;
      line-height: 16px;
    }
  }
}

/* The story on a pinned scroll. The stage holds still while the reader scrolls three
   beats: the plan's blocks land one by one; 01006021 section 3 is dragged over Tuesday
   morning, where it clashes with 01006020 and the chip names the clash; then it travels
   to Wednesday afternoon, turns green, and lands as section 5 while the readout counts
   the plan. Each beat holds still and fully readable for a stretch of scroll before the
   next begins: the landed blocks under beat one, the clash and its chip under beat two,
   the finished plan under beat three. In every browser site.js sets data-step as the
   scroll crosses each beat's line, one beat at a time, and transitions play each beat on
   its own clock in under a second and a quarter, so a fast scroll still sees it whole.
   The final state is also what a reader without motion sees. */

.ghost__lift {
  position: relative;
  height: 100%;
}

.ghost__layer {
  position: absolute;
  inset: 0;
}

.ghost__danger,
.ghost__success {
  display: flex;
  flex-direction: column;
  padding: 6px 8px 6px 12px;
  border: 2px solid;
  border-radius: 6px;
  color: #1a1a1a;
}

.ghost__danger {
  border-color: #c62828;
  background:
    repeating-linear-gradient(
      135deg,
      rgba(198, 40, 40, 0.22) 0 6px,
      transparent 6px 12px
    ),
    #fbeaea;
}

.ghost__success {
  border-color: #1b7f3b;
  background: #e7f3eb;
}

.story__chip {
  position: absolute;
  top: calc(100% + 6px);
  left: 2px;
  z-index: 3;
  flex-direction: column;
  width: max-content;
  max-width: 240px;
  padding: 6px 10px;
  border-radius: 6px;
  background: #ffffff;
  box-shadow:
    inset 3px 0 0 #c62828,
    0 4px 16px rgba(0, 0, 0, 0.3);
  color: #1a1a1a;
  font-size: 12px;
  line-height: 1.45;
}

.story__chip > span:first-child {
  color: #c62828;
  font-weight: 600;
}

/* The story pins on any window at least 540 px tall, the dedupe stage's floor. Its rows
   take their height from the window, so the label, the beat and the whole week fit under
   the header on a short phone as on a laptop. Its pinned scroll, --pinned, is three
   screens and a header, and at least 2760 px, so each beat holds whole for most of a
   second at a steady scroll on a short phone as on a laptop. */
@media (min-height: 540px) {
  .motion .story {
    --story-row: clamp(52px, (100svh - 160px) / 5, 88px);

    padding-block: 0;
  }

  .motion .story__track {
    --pinned: max(300svh + var(--header-h), 2760px);

    position: relative;
    height: calc(100svh - var(--header-h) + var(--pinned));
  }

  .motion .story__pin {
    position: sticky;
    top: var(--header-h);
    height: calc(100svh - var(--header-h));
    align-content: safe center;
  }

  .motion .story__beats {
    display: grid;
  }

  .motion .story__beat {
    grid-area: 1 / 1;
    opacity: 0;
    transform: translateY(16px);
    transition:
      opacity 200ms linear,
      transform 300ms var(--ease-out-expo);
  }

  .motion .story__ghost {
    z-index: 2;
    display: block;
  }

  .motion .story__chip {
    display: flex;
  }

  .motion .story__landed {
    display: none;
  }

  .motion .ghost__success,
  .motion .ghost__block {
    opacity: 0;
  }

  .motion .story__block.is-blocker .story__card::after {
    content: '';
    position: absolute;
    inset: -2px;
    border: 2px solid #c62828;
    border-radius: 8px;
    opacity: 0;
  }

  /* Each beat's state. The transitions on a beat play it forward; those on the state
     before it play it back, quickly. */
  .motion .story__rules,
  .motion .story__axis,
  .motion .story__day {
    transition: opacity calc(var(--dur-fade) * var(--pace)) linear;
  }

  .motion .story__pin[data-step='0'] .story__rules,
  .motion .story__pin[data-step='0'] .story__axis,
  .motion .story__pin[data-step='0'] .story__day {
    opacity: 0;
  }

  .motion .story__pin[data-step='1'] .story__beat:nth-child(1),
  .motion .story__pin[data-step='2'] .story__beat:nth-child(2),
  .motion .story__pin[data-step='3'] .story__beat:nth-child(3) {
    opacity: 1;
    transform: none;
    transition:
      opacity calc(400ms * var(--pace)) linear calc(200ms * var(--pace)),
      transform calc(700ms * var(--pace)) var(--ease-out-expo)
        calc(200ms * var(--pace));
  }

  /* Beat one, about a second: the six blocks drop in one after another. */
  .motion .story__block {
    transition:
      opacity calc(400ms * var(--pace)) linear
        calc(var(--b) * 70ms * var(--pace)),
      transform calc(600ms * var(--pace)) var(--ease-out-expo)
        calc(var(--b) * 70ms * var(--pace));
  }

  .motion .story__pin[data-step='0'] .story__block {
    opacity: 0;
    transform: translateY(-32px) scale(1.04);
    transition:
      opacity 250ms linear,
      transform 250ms var(--ease-out-expo);
  }

  /* Beat two, under a second: the dragged section lifts in over Tuesday morning, the
     block it clashes with pulses, and the chip names the clash. */
  .motion .ghost__lift {
    transition:
      opacity calc(300ms * var(--pace)) linear,
      transform calc(600ms * var(--pace)) var(--ease-out-expo);
  }

  .motion .story__pin:not([data-step='2']):not([data-step='3']) .ghost__lift {
    opacity: 0;
    transform: translateY(-24px);
    transition:
      opacity 250ms linear,
      transform 250ms var(--ease-out-expo);
  }

  .motion .story__pin[data-step='2'] .is-blocker .story__card::after {
    animation: pulse calc(1200ms * var(--pace)) linear calc(300ms * var(--pace))
      both;
  }

  .motion .story__chip {
    transition:
      opacity 200ms linear,
      transform 200ms var(--ease-out-expo);
  }

  .motion .story__pin:not([data-step='2']) .story__chip {
    opacity: 0;
    transform: translateY(8px);
  }

  .motion .story__pin[data-step='2'] .story__chip {
    transition:
      opacity calc(300ms * var(--pace)) linear calc(400ms * var(--pace)),
      transform calc(400ms * var(--pace)) var(--ease-out-expo)
        calc(400ms * var(--pace));
  }

  /* Beat three, about a second and a quarter: the section travels to Wednesday
     afternoon, turns green, lands as section 5 and settles, and the readout counts the
     plan. */
  .motion .story__ghost {
    transition: transform 350ms var(--ease-in-out);
  }

  .motion .story__pin[data-step='3'] .story__ghost {
    transform: translate(133.333%, 100%);
    transition: transform calc(var(--dur-move) * var(--pace)) var(--ease-in-out);
  }

  .motion .ghost__danger {
    transition: opacity 250ms linear;
  }

  .motion .story__pin[data-step='3'] .ghost__danger {
    opacity: 0;
  }

  .motion .story__pin[data-step='3'] .ghost__success {
    animation: success-pass calc(900ms * var(--pace)) linear both;
  }

  .motion .ghost__block {
    transition: opacity 250ms linear;
  }

  .motion .story__pin[data-step='3'] .ghost__block {
    opacity: 1;
    transition: opacity calc(300ms * var(--pace)) linear
      calc(600ms * var(--pace));
    animation: settle calc(300ms * var(--pace)) var(--ease-out-expo)
      calc(800ms * var(--pace)) both;
  }

  .motion .story__readout {
    transition:
      opacity 200ms linear,
      transform 200ms var(--ease-out-expo);
  }

  .motion .story__pin:not([data-step='3']) .story__readout {
    opacity: 0;
    transform: translateY(8px);
  }

  .motion .story__pin[data-step='3'] .story__readout {
    transition:
      opacity calc(400ms * var(--pace)) linear calc(700ms * var(--pace)),
      transform calc(500ms * var(--pace)) var(--ease-out-expo)
        calc(700ms * var(--pace));
  }
}

@media (min-width: 640px) and (max-width: 1023px) and (min-height: 540px) {
  .motion .story {
    --story-row: clamp(40px, (100svh - 340px) / 5, 72px);
  }

  .motion .story__track {
    --pinned: max(270svh + var(--header-h), 2760px);
  }
}

/* Under 630 px of height these rows fall below 58 px, too short for a two line chip under
   the Tuesday block or a time line under a block's id. The chip then sets its two parts on
   one line, which fits the empty Wednesday morning under the block without reaching the
   Thursday block below, and each block shows its id alone, with no name or time line cut
   off at its foot. */
@media (min-width: 640px) and (max-width: 1023px) and (min-height: 540px) and (max-height: 629px) {
  .motion .story__chip {
    top: calc(100% + 4px);
    flex-direction: row;
    gap: 8px;
    max-width: none;
    padding: 4px 10px;
    white-space: nowrap;
  }

  .motion .story__name,
  .motion .story__time {
    display: none;
  }
}

@media (min-width: 640px) and (min-height: 540px) and (max-height: 759px) {
  .motion .story__pin {
    gap: 24px;
  }
}

/* A phone's quarter hour tracks take what the window leaves under the text, at most the
   9 px they rest at. */
@media (max-width: 639px) and (min-height: 540px) {
  .motion .story {
    --q: clamp(5.5px, (100svh - 314px) / 40, 9px);
  }

  .motion .story__pin[data-step='3'] .story__ghost {
    transform: translate(100%, 133.333%);
  }
}

@media (max-width: 639px) and (min-height: 540px) and (max-height: 759px) {
  .motion .story__pin {
    gap: 20px;
  }

  .motion .story__beats {
    margin-top: 8px;
  }

  .motion .story__beat {
    font-size: 24px;
  }
}

/* The scene meter. A pinned scene holds still for a long stretch of scroll, which a
   reader who scrolls slowly could take for a page that has stopped working, so each
   scene keeps something moving with the scroll while its steps hold: a rail just under
   the scene's content that fills across the scene's pinned scroll, with a mark at each
   step that fills once its step has played. Every rail fills at one rate, a tenth of a
   pixel for each pixel of pinned scroll, so its width follows its track's --pinned. A
   reader who stops on a finished step finds a quiet keep scrolling cue over the rail a
   second later, gone again at the next movement. All of it is decoration on elements of
   its own, hidden from assistive technology and never on a stepped element, so it never
   fights the steps' own clock. Path A fills the rail on the track's view timeline and
   grows each mark as the scroll nears it; path B fills it from --progress, the points
   site.js watches, with a short transition from one to the next. */
.scene-meter {
  display: none;
}

@media (min-height: 540px) {
  .motion .scene-meter {
    --meter-track: var(--site-rule);
    --meter-fill: var(--site-primary);
    --meter-bg: var(--site-paper);

    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    left: 0;
    z-index: 3;
    display: grid;
    width: max-content;
    margin-inline: auto;
    justify-items: center;
    gap: 4px;
    color: var(--site-ink-soft);
    pointer-events: none;
  }

  /* Under the merged card, which ends the repeats' height above the stage's foot. */
  .motion .dedupe-stage > .scene-meter {
    top: calc(100% - var(--close) + 16px);
  }

  .motion .story .scene-meter {
    --meter-track: var(--ink-hour);
    --meter-fill: var(--focus-color);
    --meter-bg: var(--ink-bg);

    color: var(--ink-soft);
  }

  .motion .scene-cue {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 13px;
    line-height: 16px;
    opacity: 0;
    transform: translateY(4px);
    transition:
      opacity 150ms linear,
      transform 150ms var(--ease-out-expo);
  }

  .motion .scene-cue__chevron {
    width: 14px;
    height: 14px;
    animation: cue-bob 1600ms var(--ease-in-out) infinite paused;
  }

  .motion .is-idle > .scene-cue {
    opacity: 1;
    transform: none;
    transition:
      opacity 400ms linear,
      transform 600ms var(--ease-out-expo);
  }

  .motion .is-idle .scene-cue__chevron {
    animation-play-state: running;
  }

  .motion .scene-rail {
    position: relative;
    width: min(calc(var(--pinned) / 10), 100vw - 72px);
    height: 3px;
    border-radius: 2px;
    background: var(--meter-track);
  }

  .motion .scene-rail__fill {
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: var(--meter-fill);
    transform: scaleX(var(--progress, 0));
    transform-origin: left;
    transition: transform 250ms linear;
  }

  .motion .scene-rail__mark {
    position: absolute;
    top: -3px;
    left: calc(var(--at) * 100% - 4.5px);
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--meter-bg);
    box-shadow: inset 0 0 0 2px var(--meter-track);
    transition:
      background-color 300ms linear,
      box-shadow 300ms linear;
  }

  .motion .scene-rail__mark.is-passed {
    background: var(--meter-fill);
    box-shadow: inset 0 0 0 2px var(--meter-fill);
  }

  /* The dedupe rail and cue sit under the merged card, where the thirteen rows reach, so
     they show only once the merge has played. */
  .motion .dedupe-pin .scene-meter {
    opacity: 0;
    transition: opacity 200ms linear;
  }

  .motion .dedupe-pin[data-step='2'] .scene-meter {
    opacity: 1;
    transition: opacity 400ms linear 400ms;
  }
}

/* The scroll linked layer. Through every hold something in the content itself moves with
   the scroll, slowly and in one direction, while the stepped end state stays whole, in
   place and legible: a highlight lights one item, holds and hands on to the next. Each
   item names its window as shares of the pinned scroll, --from and --to; path A plays
   it on the track's view timeline over that range, path B reads it from --progress,
   which site.js steps every sixtieth of the pinned scroll and a transition smooths. A
   sweep rises over the first 40 percent of its window, holds and falls over the last 40,
   so the next item's rise is this one's fall; a ramp only rises. Every highlight is a
   ring, a lit panel under the text or a dashed slot, on a pseudo element of its own, so
   it never touches a stepped transform or the text's own contrast.

   On the merged dedupe card the four sections are read one at a time: an orange ring
   lights on a white panel inside a row, holds, and fades as the next row's lights, a
   cross fade over a stretch of the scroll in which each ring stays inside its own row.
   No ring moves, so no edge ever crosses a row's text, and a reader who stops anywhere
   finds one row lit, or two faint ones while one hands on to the next. Each row's chip,
   the count of its listings, keeps a thin ring once its row has been read, lit in the
   middle of the row's rest, so the card never returns to a state it has shown. */
@media (min-height: 540px) {
  /* The middle of each row's rest, where its chip takes its ring. */
  .motion .dedupe tbody tr[data-row='first'] {
    --from: calc(0.191 + var(--t, 0) * 0.2437);
    --to: calc(var(--from) + 0.075);
  }

  .motion .dedupe tr[data-row='first']::before,
  .motion .dedupe tr[data-row='first'] .dedupe__chip::before {
    content: '';
    position: absolute;
    opacity: 0;
    pointer-events: none;
  }

  /* Each row's ring and panel: in over the first 30 percent of the window, out over the
     last 30, so with one row every 0.2437 of the pinned scroll the next row's rise is
     this one's fall. The last row is still whole as the pin lets go. */
  .motion .dedupe tr[data-row='first']::before {
    --from: calc(0.081 + var(--t, 0) * 0.2437);
    --to: calc(var(--from) + 0.3481);

    inset: 0 0 0 4px;
    z-index: -1;
    border-radius: 0 4px 4px 0;
    background: rgba(255, 255, 255, 0.85);
    box-shadow: inset 0 0 0 2px var(--site-primary);
  }

  .motion .dedupe tr[data-row='first'] .dedupe__chip {
    position: relative;
  }

  .motion .dedupe tr[data-row='first'] .dedupe__chip::before {
    inset: 0;
    border-radius: inherit;
    box-shadow: inset 0 0 0 1px var(--site-primary);
  }

  .motion .dedupe-pin:not([data-step='2']) tr[data-row='first']::before {
    display: none;
  }

  /* On the story each beat's hold has its own motion. Under beat one, then again under
     beat three, a ring visits the placed blocks one after another, the landed section
     last, and under beat three a soft panel lights behind the credit readout. Before beat
     two a dashed slot brightens where the dragged section will drop; under beat two the
     ring turns red and visits the dragged section where it clashes, then the chip that
     names the clash, and a green slot brightens, dims a little and brightens again where
     it will land in beat three. */
  .motion .story__card {
    --from: calc(0.01 + var(--b, 0) * 0.03);
    --to: calc(var(--from) + 0.05);
    --from3: calc(0.645 + var(--b, 0) * 0.039);
    --to3: calc(var(--from3) + 0.065);
  }

  .motion .ghost__block {
    --b: 6;
    --from: 2;
    --to: 3;
  }

  .motion .story__card::before,
  .motion .ghost__danger::after,
  .motion .story__chip::after,
  .motion .story__ghost::before,
  .motion .story__ghost::after,
  .motion .story__readout::before {
    content: '';
    position: absolute;
    opacity: 0;
    pointer-events: none;
  }

  .motion .story__card::before {
    inset: -1px;
    border-radius: 7px;
    box-shadow:
      0 0 0 2px var(--focus-color),
      0 0 18px rgba(255, 138, 76, 0.35);
  }

  .motion .ghost__danger::after {
    --from: 0.28;
    --to: 0.38;

    inset: -3px;
    border-radius: 9px;
    box-shadow:
      0 0 0 2px #ff6b6b,
      0 0 18px rgba(255, 107, 107, 0.4);
  }

  .motion .story__chip::after {
    --from: 0.34;
    --to: 0.44;

    inset: -1px;
    border-radius: 7px;
    box-shadow: 0 0 0 2px #ff6b6b;
  }

  /* The slot before beat two, over the Tuesday morning the section drops onto. */
  .motion .story__ghost::after {
    --from: 0.15;
    --to: 0.245;

    inset: 0;
    border-radius: 8px;
    outline: 2px dashed var(--focus-color);
    outline-offset: -3px;
  }

  /* The slot before beat three, over the Wednesday afternoon the section travels to. */
  .motion .story__ghost::before {
    --from: 0.43;
    --to: 0.61;

    inset: 0;
    border-radius: 8px;
    background: rgba(63, 191, 111, 0.12);
    outline: 2px dashed #3fbf6f;
    outline-offset: -3px;
    transform: translate(133.333%, 100%);
  }

  .motion .story__readout {
    position: relative;
    isolation: isolate;
    justify-self: start;
  }

  .motion .story__readout::before {
    --from: 0.88;
    --to: 0.98;

    inset: -2px -8px;
    z-index: -1;
    border-radius: 6px;
    background: rgba(255, 138, 76, 0.22);
  }

  .motion .story__pin:not([data-step='1']) .story__ghost::after,
  .motion .story__pin:not([data-step='2']) .story__ghost::before,
  .motion .story__pin:not([data-step='2']) .ghost__danger::after {
    display: none;
  }
}

@media (max-width: 639px) and (min-height: 540px) {
  .motion .story__ghost::before {
    transform: translate(100%, 133.333%);
  }
}

/* Path B: the scroll linked layer and the exits read the points site.js watches. A sweep
   follows the share of its window the scroll has passed, --u, and a scene whose pin has
   let go sinks or lifts, shrinks and dims at the pace of the scroll, --leave, as the
   scroll driven exit does. site.js sets --progress on the few elements that read it, and
   --leave on the pin, which alone reads it: registered as not inherited, a new value
   restyles the pin and none of the scene inside it. */
@property --leave {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

@media (min-height: 540px) {
  /* The dedupe rows hand on at the points site.js watches, each step a short fade. */
  .motion.no-sda .dedupe tr[data-row='first']::before {
    --u: clamp(
      0,
      (var(--progress, 0) - var(--from)) / (var(--to) - var(--from)),
      1
    );

    opacity: clamp(0, min(var(--u), 1 - var(--u)) * 10 / 3, 1);
    transition: opacity 250ms linear;
  }

  .motion.no-sda .ghost__danger::after,
  .motion.no-sda .story__chip::after {
    --u: clamp(
      0,
      (var(--progress, 0) - var(--from)) / (var(--to) - var(--from)),
      1
    );

    opacity: clamp(0, min(var(--u), 1 - var(--u)) * 2.5, 1);
    transition: opacity 250ms linear;
  }

  .motion.no-sda .story__card::before {
    --u: clamp(
      0,
      (var(--progress, 0) - var(--from)) / (var(--to) - var(--from)),
      1
    );
    --u3: clamp(
      0,
      (var(--progress, 0) - var(--from3)) / (var(--to3) - var(--from3)),
      1
    );

    opacity: clamp(
      0,
      max(min(var(--u), 1 - var(--u)), min(var(--u3), 1 - var(--u3))) * 2.5,
      1
    );
    transition: opacity 250ms linear;
  }

  .motion.no-sda .dedupe tr[data-row='first'] .dedupe__chip::before,
  .motion.no-sda .story__ghost::before,
  .motion.no-sda .story__ghost::after,
  .motion.no-sda .story__readout::before {
    opacity: clamp(
      0,
      (var(--progress, 0) - var(--from)) / (var(--to) - var(--from)),
      1
    );
    transition: opacity 250ms linear;
  }

  .motion.no-sda .dedupe-pin,
  .motion.no-sda .story__pin {
    opacity: calc(1 - 0.8 * var(--leave, 0));
    transition:
      opacity 250ms linear,
      transform 250ms linear;
  }

  .motion.no-sda .dedupe-pin {
    transform: translateY(calc(14svh * var(--leave, 0)))
      scale(calc(1 - 0.06 * var(--leave, 0)));
  }

  .motion.no-sda .story__pin {
    transform: translateY(calc(-6svh * var(--leave, 0)))
      scale(calc(1 - 0.02 * var(--leave, 0)));
  }
}

/* Path A. Each pinned track is a view timeline measured below the header, so its contain
   range is exactly the pinned scroll, the stretch the rail fills and the highlights move
   through, and its exit range is the scroll over which the released stage leaves the
   window. The dedupe stage sinks a little slower than the page, shrinks and dims as the
   dark band rises over it; the story lifts a little faster than its band and dims as the
   next section comes up. A block that the ring visits twice, under beat one and beat
   three, plays two sweeps that each fill nothing outside their own window. */
@supports (animation-timeline: view()) {
  @media (min-height: 540px) {
    .motion:not(.no-sda) .dedupe-track,
    .motion:not(.no-sda) .story__track {
      view-timeline: --scene block;
      view-timeline-inset: var(--header-h) 0px;
    }

    .motion:not(.no-sda) .scene-rail__fill {
      animation: rail-fill linear both;
      animation-timeline: --scene;
      animation-range: contain 0% contain 100%;
    }

    .motion:not(.no-sda) .scene-rail__mark {
      animation: mark-near linear both;
      animation-timeline: --scene;
      animation-range: contain calc(var(--at) * 100% - 12%) contain
        calc(var(--at) * 100%);
    }

    .motion:not(.no-sda) .dedupe-pin {
      animation: dedupe-leave linear both;
      animation-timeline: --scene;
      animation-range: exit 0% exit 80%;
    }

    .motion:not(.no-sda) .story__pin {
      animation: story-leave linear both;
      animation-timeline: --scene;
      animation-range: exit 0% exit 80%;
    }

    .motion:not(.no-sda) .dedupe tr[data-row='first']::before {
      animation: hand-on linear both;
      animation-timeline: --scene;
      animation-range: contain calc(var(--from) * 100%) contain
        calc(var(--to) * 100%);
    }

    .motion:not(.no-sda) .ghost__danger::after,
    .motion:not(.no-sda) .story__chip::after {
      animation: sweep linear both;
      animation-timeline: --scene;
      animation-range: contain calc(var(--from) * 100%) contain
        calc(var(--to) * 100%);
    }

    .motion:not(.no-sda) .story__card::before {
      animation:
        sweep linear,
        sweep linear;
      animation-timeline: --scene, --scene;
      animation-range:
        contain calc(var(--from) * 100%) contain calc(var(--to) * 100%),
        contain calc(var(--from3) * 100%) contain calc(var(--to3) * 100%);
    }

    .motion:not(.no-sda) .dedupe tr[data-row='first'] .dedupe__chip::before,
    .motion:not(.no-sda) .story__ghost::after,
    .motion:not(.no-sda) .story__readout::before {
      animation: brighten linear both;
      animation-timeline: --scene;
      animation-range: contain calc(var(--from) * 100%) contain
        calc(var(--to) * 100%);
    }

    .motion:not(.no-sda) .story__ghost::before {
      animation: beckon linear both;
      animation-timeline: --scene;
      animation-range: contain calc(var(--from) * 100%) contain
        calc(var(--to) * 100%);
    }
  }
}

/* On the shorter windows the pinned track runs on its pixel floor, so a row's full rest,
   40 percent of its window at 30 percent fades, held the card unchanged for close to
   200 px of scroll, and where the compact rows hide the listing chips nothing else moved
   through it. There each row fades in and out over 35 percent of a window of 0.3749, the
   same row every 0.2437, so the fall of one row is still the rise of the next, the rest
   shrinks to 30 percent of the window and the last row is still whole as the pin lets go.
   The chip takes its ring from 0.15 of the row's window, the middle of the shorter rest. */
@media (max-width: 639px) and (min-height: 540px) and (max-height: 759px),
  (min-width: 640px) and (min-height: 540px) and (max-height: 819px) {
  .motion .dedupe tbody tr[data-row='first'] {
    --from: calc(0.231 + var(--t, 0) * 0.2437);
  }

  .motion .dedupe tr[data-row='first']::before {
    --to: calc(var(--from) + 0.3749);
  }

  .motion.no-sda .dedupe tr[data-row='first']::before {
    opacity: clamp(0, min(var(--u), 1 - var(--u)) * 20 / 7, 1);
  }

  @supports (animation-timeline: view()) {
    .motion:not(.no-sda) .dedupe tr[data-row='first']::before {
      animation-name: hand-on-short;
    }
  }
}

@keyframes cue-bob {
  0%,
  100% {
    transform: none;
  }

  50% {
    transform: translateY(3px);
  }
}

@keyframes rail-fill {
  from {
    transform: scaleX(0);
  }

  to {
    transform: none;
  }
}

@keyframes mark-near {
  to {
    transform: scale(1.4);
  }
}

@keyframes sweep {
  0%,
  100% {
    opacity: 0;
  }

  40%,
  60% {
    opacity: 1;
  }
}

@keyframes brighten {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

/* The slot beat three lands on brightens, dims a little once and brightens again, so
   it stands whole as beat three lands. */
@keyframes beckon {
  0% {
    opacity: 0;
  }

  30%,
  70%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.45;
  }
}

/* A dedupe row's ring and panel, whose fall is the next row's rise. */
@keyframes hand-on {
  0%,
  100% {
    opacity: 0;
  }

  30%,
  70% {
    opacity: 1;
  }
}

/* A dedupe row's ring and panel on the shorter windows, with longer fades. */
@keyframes hand-on-short {
  0%,
  100% {
    opacity: 0;
  }

  35%,
  65% {
    opacity: 1;
  }
}

@keyframes dedupe-leave {
  to {
    opacity: 0.2;
    transform: translateY(14svh) scale(0.94);
  }
}

@keyframes story-leave {
  to {
    opacity: 0.2;
    transform: translateY(-6svh) scale(0.98);
  }
}

@keyframes pulse {
  0%,
  100% {
    opacity: 0;
  }

  25%,
  75% {
    opacity: 1;
  }

  50% {
    opacity: 0.3;
  }
}

@keyframes success-pass {
  0%,
  100% {
    opacity: 0;
  }

  25%,
  62.5% {
    opacity: 1;
  }
}

@keyframes settle {
  from {
    transform: scale(1.03);
  }

  to {
    transform: none;
  }
}

/* On a phone the dragged section's skins are set a step smaller, and the clash chip is
   one line in the empty noon hour under the Tuesday morning block, so it never covers the
   afternoon block below; the pulsing block beside it already shows the time. */
@media (max-width: 639px) {
  .ghost__danger,
  .ghost__success {
    padding: 8px 2px 4px;
    font-size: 10px;
  }

  .story__chip {
    top: calc(100% + 1px);
    max-width: calc(400% - 6px);
    padding: 2px 8px;
    font-size: 11px;
    line-height: 16px;
    white-space: nowrap;
  }

  .story__chip > span:last-child {
    display: none;
  }
}

/* Plate parallax: on a wide window each feature screen's visual drifts against the page
   as it passes, the phone wallpaper faster than the poster behind it, so it reads as a
   nearer layer. Path A only; elsewhere the plates use the plain reveal. */
@supports (animation-timeline: view()) {
  @media (min-width: 768px) {
    .motion:not(.no-sda) .exam__plate {
      animation: drift-exam linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    .motion:not(.no-sda) .poster--wide {
      animation: drift-wide linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    .motion:not(.no-sda) .poster--phone {
      animation: drift-phone linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }

    .motion:not(.no-sda) .plans__plate {
      animation: plate-in linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 45%;
    }
  }
}

@keyframes drift-exam {
  from {
    transform: translateY(64px);
  }

  to {
    transform: translateY(-24px);
  }
}

@keyframes drift-wide {
  from {
    transform: translateY(32px);
  }

  to {
    transform: translateY(-32px);
  }
}

@keyframes drift-phone {
  from {
    transform: translateY(120px);
  }

  to {
    transform: translateY(-60px);
  }
}

@keyframes plate-in {
  from {
    transform: scale(0.94) translateY(48px);
    animation-timing-function: var(--ease-out-expo);
  }

  to {
    transform: none;
  }
}

/* The closing call to action. A soft orange glow stands behind the mark; it is
   decoration, so it shows at rest too. As the band comes into view the glow grows, the
   mark turns upright and the heading, the line under it and each button rise in turn.
   The band clips the glow without becoming a scroll container, so the heading and the
   badges can follow their own view of the window. */
.download {
  position: relative;
  overflow: hidden;
  overflow: clip;
}

.download::before {
  content: '';
  position: absolute;
  top: calc(var(--space-section) + 36px - 210px);
  left: calc(50% - 210px);
  width: 420px;
  height: 420px;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgba(227, 82, 5, 0.16),
    transparent
  );
  pointer-events: none;
}

.download__inner {
  position: relative;
}

@media (min-width: 1024px) {
  .download::before {
    top: calc(var(--space-section) + 44px - 300px);
    left: calc(50% - 300px);
    width: 600px;
    height: 600px;
  }
}

.motion .download .statement__h {
  --e: 0;
}

.motion .download .cta-row {
  --e: 1;
}

@supports (animation-timeline: view()) {
  .motion:not(.no-sda) .download {
    view-timeline: --close block;
  }

  .motion:not(.no-sda) .download::before {
    animation: glow-in linear both;
    animation-timeline: --close;
    animation-range: entry 20% cover 50%;
  }

  /* The mark, the heading and the badges are the page's last call to action, so each
     rises on its own view like a revealed block and stands at full strength once its
     top is 85 percent down the window, where a reader at rest finds it. The mark sits
     above the heading, so it is upright before the heading has risen. */
  .motion:not(.no-sda) .download__mark,
  .motion:not(.no-sda) .download .statement__h,
  .motion:not(.no-sda) .download .cta-row {
    animation: rise linear both;
    animation-timeline: view();
    animation-range: entry min(10%, 2svh) entry max(min(70%, 15svh), 96px);
    --rise: 24px;
  }

  .motion:not(.no-sda) .download__mark {
    animation-name: mark-in;
  }
}

.motion.no-sda .download::before,
.motion.no-sda .download__mark,
.motion.no-sda .download .statement__h,
.motion.no-sda .download .cta-row {
  transition:
    opacity var(--dur-reveal) var(--ease-out-expo),
    transform var(--dur-reveal) var(--ease-out-expo);
}

.motion.no-sda .download .statement__h,
.motion.no-sda .download .cta-row {
  transition-delay: calc(150ms + 85ms * var(--e));
}

.motion.no-sda .download:not(.is-in)::before {
  opacity: 0;
  transform: scale(0.6);
}

.motion.no-sda .download:not(.is-in) .download__mark {
  opacity: 0;
  transform: scale(0.7) rotate(-10deg);
}

.motion.no-sda .download:not(.is-in) .statement__h,
.motion.no-sda .download:not(.is-in) .cta-row {
  opacity: 0;
  transform: translateY(calc(24px * var(--k)));
}

/* A badge the reader tabs to shows at full strength at once, wherever the scroll has
   left its rise: moving focus scrolls the badge only just into view, short of where the
   band finishes rising. */
.motion .download .cta-row:focus-within {
  animation: none;
  opacity: 1;
  transform: none;
}

@keyframes glow-in {
  from {
    opacity: 0;
    transform: scale(0.6);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes mark-in {
  from {
    opacity: 0;
    transform: scale(0.7) rotate(-10deg);
    animation-timing-function: var(--ease-out-expo);
  }

  to {
    opacity: 1;
    transform: none;
  }
}
