/* Motion.
   ────────────────────────────────────────────────────────────────────────────────────────────────────────
   Two kinds, for two different jobs.

   Things that ARRIVE — sections, cards, schedule rows, questions — are revealed by /redline-ink.mjs as you
   reach them, and play over a fixed time. Scrubbed to the scroll position, as they first were, they mostly
   read as content that happened to be there: a quick scroll made them instant. Played over time, they read
   as motion.

   Things that are DRAWN BY THE SCROLL — the red line across the top, the door swinging open, the bars under
   NET AREA, and the A101 sheet beside "How drawing review works" — stay tied to the scroll position, because
   for those the scroll is the pen. Scrolling back undraws them.

   One rule over both: the resting state of every element is its FINISHED state. Nothing is hidden unless
   the ink script has run, proved it can show it again, and marked the page `ink-ready`, or unless the engine
   itself scrubs it to the scroll. No script, a script that fails, a reader who asked for less motion, or a
   printed page — each gets the finished page.

   Nothing on the page draws when the pointer moves or clicks.

   home.css starts every element at no animation and opts things back in with !important, which is why the
   declarations below carry it. Only opacity, transform and stroke-dashoffset move, so CLS stays at zero. */

/* ── arriving ─────────────────────────────────────────────────────────────────────────────────────────── */
@media screen and (prefers-reduced-motion: no-preference) {
  /* Hidden instantly, revealed over time. The transition sits on the revealed state only, so a section
     marked hidden below the fold does not visibly fade out on its way to being hidden. */
  .ink-ready .home .reveal:not(.is-in) { opacity: 0; transform: translateY(34px); }
  .ink-ready .home .reveal.is-in {
    transition: opacity .75s cubic-bezier(.2,.7,.2,1) var(--d, 0ms), transform .95s cubic-bezier(.16,.84,.24,1) var(--d, 0ms);
  }
  /* A card is a heavier object than a line of text: it comes from further, and settles with a slight scale. */
  .ink-ready .home :is(.feature, .price-card, .schedule, .matrix).reveal:not(.is-in) { transform: translateY(56px) scale(.97); }
  /* A schedule row or a question is a single line in a list; it only needs to be set down, not thrown. */
  .ink-ready .home :is(.sched-row, .faq-list details).reveal:not(.is-in) { transform: translateY(14px); }

  /* Opacity does not take anything out of the tab order, so someone moving through the page with a keyboard can
     land on a link inside a section that has not arrived yet. Whatever holds focus is shown, at once. */
  .ink-ready .home .reveal:focus-within { opacity: 1 !important; transform: none !important; }

}

/* ── drawn by the scroll ─────────────────────────────────────────────────────────────────────────────── */
@media screen and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {

    /* A red line across the top that grows as you read. The company is called Redline; the progress of a
       drawing review is a red line getting longer. Two pixels, and it never asks for attention. */
    .home::before {
      content: '';
      position: fixed;
      inset: 0 auto auto 0;
      z-index: 60;
      width: 100%;
      height: 2px;
      background: var(--red);
      transform-origin: 0 50%;
      transform: scaleX(0);
      animation: rl-progress linear both !important;
      animation-timeline: scroll(root block) !important;
    }
    @keyframes rl-progress { to { transform: scaleX(1); } }

    /* The door swings open as you scroll past it: the one illustration on the page that is literally a
       drawing being made, and here the scroll is the pen. 420 is comfortably longer than the path — 150 of
       leaf plus about 236 of quarter-circle at r=150 — so it starts wholly hidden and ends wholly drawn. */
    .home .architecture svg path:nth-of-type(2) {
      stroke-dasharray: 420;
      stroke-dashoffset: 0;
      animation: rl-ink linear both !important;
      animation-timeline: view() !important;
      animation-range: entry 12% cover 42% !important;
    }
    @keyframes rl-ink { from { stroke-dashoffset: 420; } }

    /* The chord, the hinge dots and the dimension are what a draughtsman adds once the swing is drawn. */
    .home .architecture svg path:nth-of-type(3),
    .home .architecture svg g,
    .home .architecture svg text,
    .home .architecture svg path:nth-of-type(4) {
      animation: rl-fade linear both !important;
      animation-timeline: view() !important;
      animation-range: entry 34% cover 52% !important;
    }
    @keyframes rl-fade { from { opacity: 0; } }

    /* The service run's centreline is already dashed in the markup (4 5), so moving the offset does not draw
       it — it moves the dashes along it, which is how a pipe or duct is shown to be carrying something. Four
       periods of travel reads as flow rather than a blur. */
    .home .engineering svg path:nth-of-type(3) {
      stroke-dashoffset: 0;
      animation: rl-run linear both !important;
      animation-timeline: view() !important;
      animation-range: entry 14% cover 60% !important;
    }
    @keyframes rl-run { from { stroke-dashoffset: 36; } }
    .home .engineering svg circle,
    .home .engineering svg path:nth-of-type(4) {
      animation: rl-fade linear both !important;
      animation-timeline: view() !important;
      animation-range: entry 36% cover 54% !important;
    }

    /* The bars under NET AREA rise one after another as the card comes up the screen: the quantity being
       taken off. */
    .home .takeoff .quantity i {
      transform-origin: 50% 100%;
      animation: rl-rise linear both !important;
      animation-timeline: view() !important;
    }
    .home .takeoff .quantity i:nth-child(1) { animation-range: cover 6% cover 30% !important; }
    .home .takeoff .quantity i:nth-child(2) { animation-range: cover 10% cover 34% !important; }
    .home .takeoff .quantity i:nth-child(3) { animation-range: cover 14% cover 38% !important; }
    .home .takeoff .quantity i:nth-child(4) { animation-range: cover 18% cover 42% !important; }
    .home .takeoff .quantity i:nth-child(5) { animation-range: cover 22% cover 46% !important; }
    @keyframes rl-rise { from { transform: scaleY(.06); } }
  }
}

/* ── the review sheet ────────────────────────────────────────────────────────────────────────────────────
   The A101 sheet beside "How drawing review works" is opened, calibrated, marked up, measured, counted and issued
   by the scroll, each part beside the step that describes it:

     beat 0  the heading      the sheet settles, the walls ink round, openings and furniture are set in
     beat 1  step 01          a cursor clicks grid line 1 and drags the dimension to grid line 2, its figure
                              counting to 5 400; the title block's NOT SET becomes 1 : 100
     beat 2  step 02          a revision cloud bubbles round the bedroom, the callout is led to the door, the
                              kitchen is traced, washes in and reads 14.49 m², four windows are counted, and the
                              markups list under the sheet fills row by row
     beat 3  step 03          the REVIEWED stamp lands, and PDF and CSV are issued

   Every moving part has one @keyframes, named by --k, with its timing inside its beat written in the keyframe
   percentages. Two motors play the same keyframes:
     - the engine, where it has view timelines: each beat is the view timeline of the element that drives it. The
       heading's runs while its top moves from 100% to 45% of the window. A step's starts when its top is 80% of the
       way down the window and lasts a set distance of scroll, so a beat feels the same on any screen: 340px for
       step 01, 500px for step 02, which draws five markups, and 320px for step 03, each held to a share of a short
       window (46%, 62%, 44%) so the step is still on screen when its beat ends. Each step is as tall as its beat,
       so one beat hands over to the next. On a phone the four beats share the sheet's own pass up the screen.
     - /redline-ink.mjs elsewhere: it works out each beat's progress from the same ranges, and moves the paused
       animations to it through the Web Animations API.
   The figures that count (5 400, 14.49 m², the list's 4) are written by the script; the markup holds their final
   values. DESK and PHONE in /redline-ink.mjs must match the animation-range values below. */
.rv-lift { --k: rv-lift; }      .rv-grid { --k: rv-grid; }      .rv-wall { --k: rv-wall; }
.rv-part { --k: rv-part; }      .rv-part2 { --k: rv-part2; }    .rv-open { --k: rv-open; }
.rv-furn { --k: rv-furn; }
.rv-cur1 { --k: rv-cur1; }      .rv-pulseA { --k: rv-pulseA; }  .rv-pulseB { --k: rv-pulseB; }
.rv-extA { --k: rv-extA; }      .rv-extB { --k: rv-extB; }      .rv-tickA { --k: rv-tickA; }
.rv-tickB { --k: rv-tickB; }    .rv-dim { --k: rv-dim; }        .rv-dimlabel { --k: rv-dimlabel; }
.rv-notset { --k: rv-notset; }  .rv-scale { --k: rv-scale; }    .rv-row1 { --k: rv-row1; }
.rv-empty { --k: rv-empty; }
.rv-cur2 { --k: rv-cur2; }      .rv-cloud { --k: rv-cloud; }    .rv-callout { --k: rv-callout; }
.rv-lead { --k: rv-lead; }      .rv-dot { --k: rv-dot; }        .rv-edge { --k: rv-edge; }
.rv-flood { --k: rv-flood; }    .rv-hatch { --k: rv-hatch; }    .rv-arealabel { --k: rv-arealabel; }
.rv-m1 { --k: rv-m1; }          .rv-m2 { --k: rv-m2; }          .rv-m3 { --k: rv-m3; }
.rv-m4 { --k: rv-m4; }          .rv-row2 { --k: rv-row2; }      .rv-row3 { --k: rv-row3; }
.rv-row4 { --k: rv-row4; }
.rv-cur3 { --k: rv-cur3; }      .rv-pulseS { --k: rv-pulseS; }  .rv-stamp { --k: rv-stamp; }
.rv-pdf { --k: rv-pdf; }        .rv-csv { --k: rv-csv; }        .rv-tally { --k: none; }
/* The figures count only while /redline-ink.mjs runs, and it marks the page rv-count when it does. Without it (no
   JavaScript, or a script that failed, in an engine that still scrubs the drawing), a figure shows only once the line, the
   wash or the list it gives is complete, so 5 400 never stands beside a line half that long. */
:root:not(.rv-count) .rv-dimlabel { --k: rv-dimlabel-still; }
:root:not(.rv-count) .rv-arealabel { --k: rv-arealabel-still; }
:root:not(.rv-count) .rv-tally { --k: rv-tally; }

@media screen and (prefers-reduced-motion: no-preference) {
  /* Room for the steps to pass the pinned sheet, in the layout from the first paint, so nothing shifts when the
     script arrives. Each step is as tall as its beat (--rv-l1..3, the scroll each beat lasts; DESK in
     /redline-ink.mjs states the same lengths). The last step is taller, so the sheet stays pinned until PDF and CSV
     are issued even on a short window, where the sheet and its list fill the window under the header. */
  @media (min-width: 761px) {
    .home .product-proof { --rv-l1: min(340px, 46vh); --rv-l2: min(500px, 62vh); --rv-l3: min(320px, 44vh); }
    .home .product-proof > div li { box-sizing: border-box; }
    .home .product-proof > div li:nth-child(1) { min-height: var(--rv-l1); }
    .home .product-proof > div li:nth-child(2) { min-height: var(--rv-l2); }
    .home .product-proof > div li:nth-child(3) { min-height: calc(20vh + 260px); }
    /* The step being drawn fills its number. */
    .home .product-proof > div li::after {
      /* The number is already read out once, from ::before; this copy is for the eye only. */
      content: '0' counter(review) / '';
      position: absolute; left: 0; top: 22px;
      width: 30px; height: 30px; box-sizing: border-box;
      display: grid; place-items: center;
      border: 1px solid var(--red); border-radius: 50%;
      background: var(--red); color: #fff; font-size: 11px;
      opacity: 0; pointer-events: none;
    }
  }
  /* What the keyframes need: lines drawn by an offset along a length of 1, and pops scaled about their own middle.
     The gap is longer than any line, so no sliver of a line shows before it is drawn. The cloud's arcs come out a
     little longer when they are dashed than the length they report, so its dash runs a little past 1: the finished
     cloud closes at its first corner instead of leaving a gap there (and, undrawn, a red speck). */
  .home .sheet-art [pathLength] { stroke-dasharray: 1 2; }
  .home .sheet-art .rv-cloud { stroke-dasharray: 1.01 2; }
  .home .sheet-art :is(.rv-dot, .rv-m1, .rv-m2, .rv-m3, .rv-m4, .rv-stamp, .rv-pdf, .rv-csv, .pulse) { transform-box: fill-box; transform-origin: center; }
  .home .sheet-art .rv-scale { transform-box: fill-box; transform-origin: 0 50%; }
  .home .sheet-art .rv-flood { transform-box: fill-box; transform-origin: 50% 100%; }
  .home .sheet-plate.rv { transform-origin: 50% 0; }
  /* A phone shows the drawing being made without the hand making it: at 390 px the cursor is a speck. */
  @media (max-width: 760px) { .home .sheet-art .hand { display: none; } }

  /* The engine's motor. */
  @supports (animation-timeline: view()) and (timeline-scope: --a) {
    .home .product-proof { timeline-scope: --rv0, --rv1, --rv2, --rv3, --rvs; }
    .home .product-proof h2 { view-timeline-name: --rv0; }
    .home .product-proof > div li:nth-child(1) { view-timeline-name: --rv1; }
    .home .product-proof > div li:nth-child(2) { view-timeline-name: --rv2; }
    .home .product-proof > div li:nth-child(3) { view-timeline-name: --rv3; }
    .home .proof-sheet { view-timeline-name: --rvs; }
    .home .proof-sheet .rv { animation: var(--k) linear both !important; }
    .home .product-proof .rv.b0 { animation-timeline: --rv0 !important; animation-range: cover 0vh cover 55vh !important; }
    .home .product-proof .rv.b1 { animation-timeline: --rv1 !important; animation-range: cover 20vh cover calc(20vh + var(--rv-l1)) !important; }
    .home .product-proof .rv.b2 { animation-timeline: --rv2 !important; animation-range: cover 20vh cover calc(20vh + var(--rv-l2)) !important; }
    .home .product-proof .rv.b3 { animation-timeline: --rv3 !important; animation-range: cover 20vh cover calc(20vh + var(--rv-l3)) !important; }
    @media (min-width: 761px) {
      .home .product-proof > div li::after { animation: rv-step linear both !important; }
      .home .product-proof > div li:nth-child(1)::after { animation-timeline: --rv1 !important; animation-range: cover 20vh cover calc(20vh + var(--rv-l1)) !important; }
      .home .product-proof > div li:nth-child(2)::after { animation-timeline: --rv2 !important; animation-range: cover 20vh cover calc(20vh + var(--rv-l2)) !important; }
      .home .product-proof > div li:nth-child(3)::after { animation-timeline: --rv3 !important; animation-range: cover 20vh cover calc(20vh + var(--rv-l3)) !important; }
    }
    @media (max-width: 760px) {
      .home .product-proof .rv.b0 { animation-timeline: --rvs !important; animation-range: cover 0vh cover 30vh !important; }
      .home .product-proof .rv.b1 { animation-timeline: --rvs !important; animation-range: cover 22vh cover 48vh !important; }
      .home .product-proof .rv.b2 { animation-timeline: --rvs !important; animation-range: cover 42vh cover 72vh !important; }
      .home .product-proof .rv.b3 { animation-timeline: --rvs !important; animation-range: cover 66vh cover 88vh !important; }
    }
  }

  /* The script's motor, for engines without view timelines: the same animations, paused, which the script moves
     to the point the scroll has reached. The -1s delay holds each one at its end until it is moved, so anything the
     script does not reach stays finished. */
  .rv-js .home .proof-sheet .rv { animation: var(--k) 1s linear -1s both paused !important; }
  @media (min-width: 761px) {
    .rv-js .home .product-proof > div li::after { animation: rv-step 1s linear -1s both paused !important; }
  }
}

/* Beat 0: the sheet is opened. */
@keyframes rv-lift  { 0% { transform: translateY(36px) rotate(-1.4deg); animation-timing-function: cubic-bezier(.2,.7,.25,1); } 62%, 100% { transform: none; } }
@keyframes rv-grid  { 0% { opacity: 0; } 22%, 100% { opacity: 1; } }
@keyframes rv-wall  { 0%, 6% { stroke-dashoffset: 1; animation-timing-function: cubic-bezier(.45,0,.35,1); } 50%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-part  { 0%, 42% { stroke-dashoffset: 1; animation-timing-function: ease-in-out; } 64%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-part2 { 0%, 56% { stroke-dashoffset: 1; animation-timing-function: ease-in-out; } 76%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-open  { 0%, 70% { opacity: 0; } 84%, 100% { opacity: 1; } }
@keyframes rv-furn  { 0%, 78% { opacity: 0; } 96%, 100% { opacity: 1; } }

/* Beat 1: calibrated. The cursor clicks grid line 1 at 13%, drags the dimension to grid line 2 between 16% and 56%
   (the line, the cursor and the riding label move together, linearly, so the figure the script counts stays under
   the cursor), clicks at 56%, then sets the scale in the title block. */
@keyframes rv-cur1 {
  0% { opacity: 0; transform: translate(236px, 150px); animation-timing-function: cubic-bezier(.3,.6,.3,1); }
  5% { opacity: 1; }
  13%, 16% { transform: translate(70px, 66px); animation-timing-function: linear; }
  56%, 60% { transform: translate(290px, 66px); animation-timing-function: cubic-bezier(.4,0,.3,1); }
  74%, 82% { transform: translate(282px, 386px); }
  88% { opacity: 1; }
  96%, 100% { opacity: 0; transform: translate(300px, 396px); }
}
@keyframes rv-pulseA { 0%, 12% { opacity: 0; transform: scale(.3); } 13% { opacity: .9; transform: scale(.4); } 22%, 100% { opacity: 0; transform: scale(1.5); } }
@keyframes rv-pulseB { 0%, 55% { opacity: 0; transform: scale(.3); } 56% { opacity: .9; transform: scale(.4); } 65%, 100% { opacity: 0; transform: scale(1.5); } }
@keyframes rv-extA   { 0%, 12% { stroke-dashoffset: 1; } 16%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-extB   { 0%, 56% { stroke-dashoffset: 1; } 60%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-tickA  { 0%, 13% { opacity: 0; } 15%, 100% { opacity: 1; } }
@keyframes rv-tickB  { 0%, 56% { opacity: 0; } 58%, 100% { opacity: 1; } }
@keyframes rv-dim    { 0%, 16% { stroke-dashoffset: 1; } 56%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-dimlabel { 0%, 14% { opacity: 0; transform: translateX(-110px); } 16% { opacity: 1; transform: translateX(-110px); } 56%, 100% { opacity: 1; transform: none; } }
@keyframes rv-dimlabel-still { 0%, 56% { opacity: 0; } 60%, 100% { opacity: 1; } }
@keyframes rv-notset { 0%, 70% { opacity: 1; } 76%, 100% { opacity: 0; } }
@keyframes rv-scale  { 0%, 73% { opacity: 0; transform: scale(1.35); } 79% { opacity: 1; transform: scale(.96); } 83%, 100% { opacity: 1; transform: none; } }
@keyframes rv-row1   { 0%, 60% { opacity: 0; transform: translateY(10px); } 66%, 100% { opacity: 1; transform: none; } }
/* The list's empty state, shown until the first row arrives; at rest (the finished sheet) it is hidden. */
@keyframes rv-empty  { 0%, 52% { opacity: 1; } 59%, 100% { opacity: 0; } }

/* Beat 2: marked up and measured. The cloud is drawn 4%–30% while the cursor runs round it (its sides are 34%, 16%,
   34% and 16% of its length); the callout pops, its leader is pulled to the door 35%–41%; the kitchen's inside faces
   are traced 48%–70% (194, 124, 194 and 124 units); the area washes in 70%–86% while its figure rises; the windows
   are counted 86%–98%; the list fills as each markup is finished. */
@keyframes rv-cur2 {
  0% { opacity: 0; transform: translate(340px, 150px); animation-timing-function: ease-out; }
  4% { opacity: 1; transform: translate(298px, 100px); animation-timing-function: linear; }
  12.74% { transform: translate(484px, 100px); animation-timing-function: linear; }
  17% { transform: translate(484px, 192px); animation-timing-function: linear; }
  25.74% { transform: translate(298px, 192px); animation-timing-function: linear; }
  30% { transform: translate(298px, 100px); animation-timing-function: ease-in-out; }
  33%, 35% { transform: translate(334px, 66px); animation-timing-function: linear; }
  41% { transform: translate(296px, 196px); animation-timing-function: ease-in-out; }
  46%, 48% { transform: translate(293px, 203px); animation-timing-function: linear; }
  54.71% { transform: translate(487px, 203px); animation-timing-function: linear; }
  59% { transform: translate(487px, 327px); animation-timing-function: linear; }
  65.71% { transform: translate(293px, 327px); animation-timing-function: linear; }
  70% { transform: translate(293px, 203px); animation-timing-function: ease-in-out; }
  76% { opacity: 1; transform: translate(338px, 296px); }
  84%, 100% { opacity: 0; transform: translate(338px, 296px); }
}
@keyframes rv-cloud    { 0%, 4% { stroke-dashoffset: 1.01; } 30%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-callout  { 0%, 31% { opacity: 0; transform: translateY(5px); } 35%, 100% { opacity: 1; transform: none; } }
@keyframes rv-lead     { 0%, 35% { stroke-dashoffset: 1; } 41%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-dot      { 0%, 40.5% { opacity: 0; transform: scale(2.2); } 42.5%, 100% { opacity: 1; transform: none; } }
@keyframes rv-edge     { 0%, 48% { stroke-dashoffset: 1; } 70%, 100% { stroke-dashoffset: 0; } }
@keyframes rv-flood    { 0%, 69.9% { opacity: 0; transform: scaleY(0); } 70% { opacity: 1; transform: scaleY(0); } 86%, 100% { opacity: 1; transform: none; } }
@keyframes rv-hatch    { 0%, 74% { opacity: 0; } 88%, 100% { opacity: 1; } }
@keyframes rv-arealabel { 0%, 70% { opacity: 0; transform: translateY(4px); } 73%, 100% { opacity: 1; transform: none; } }
@keyframes rv-arealabel-still { 0%, 86% { opacity: 0; transform: translateY(4px); } 89%, 100% { opacity: 1; transform: none; } }
@keyframes rv-tally { 0%, 97% { opacity: 0; } 99%, 100% { opacity: 1; } }
@keyframes rv-m1 { 0%, 86% { opacity: 0; transform: scale(1.9); } 88% { opacity: 1; transform: scale(.9); } 89.5%, 100% { opacity: 1; transform: none; } }
@keyframes rv-m2 { 0%, 89% { opacity: 0; transform: scale(1.9); } 91% { opacity: 1; transform: scale(.9); } 92.5%, 100% { opacity: 1; transform: none; } }
@keyframes rv-m3 { 0%, 92% { opacity: 0; transform: scale(1.9); } 94% { opacity: 1; transform: scale(.9); } 95.5%, 100% { opacity: 1; transform: none; } }
@keyframes rv-m4 { 0%, 95% { opacity: 0; transform: scale(1.9); } 97% { opacity: 1; transform: scale(.9); } 98.5%, 100% { opacity: 1; transform: none; } }
@keyframes rv-row2 { 0%, 41% { opacity: 0; transform: translateY(10px); } 47%, 100% { opacity: 1; transform: none; } }
@keyframes rv-row3 { 0%, 86% { opacity: 0; transform: translateY(10px); } 92%, 100% { opacity: 1; transform: none; } }
@keyframes rv-row4 { 0%, 94% { opacity: 0; transform: translateY(10px); } 99%, 100% { opacity: 1; transform: none; } }

/* Beat 3: issued. The cursor presses at 17% and the stamp lands, oversized and then settling; PDF and CSV follow. */
@keyframes rv-cur3 {
  0% { opacity: 0; transform: translate(250px, 250px); animation-timing-function: cubic-bezier(.3,.6,.3,1); }
  6% { opacity: 1; }
  15%, 19% { transform: translate(180px, 170px); animation-timing-function: cubic-bezier(.4,0,.3,1); }
  41%, 45% { transform: translate(432px, 388px); animation-timing-function: cubic-bezier(.4,0,.3,1); }
  55%, 59% { transform: translate(494px, 388px); }
  66% { opacity: 1; }
  76%, 100% { opacity: 0; transform: translate(516px, 404px); }
}
@keyframes rv-pulseS { 0%, 16% { opacity: 0; transform: scale(.3); } 17% { opacity: .9; transform: scale(.4); } 27%, 100% { opacity: 0; transform: scale(1.7); } }
@keyframes rv-stamp  { 0%, 17% { opacity: 0; transform: scale(1.9); } 24% { opacity: 1; transform: scale(.94); } 28%, 100% { opacity: 1; transform: none; } }
@keyframes rv-pdf    { 0%, 43% { opacity: 0; transform: scale(.6); } 48%, 100% { opacity: 1; transform: none; } }
@keyframes rv-csv    { 0%, 57% { opacity: 0; transform: scale(.6); } 62%, 100% { opacity: 1; transform: none; } }

/* The step being drawn: its number fills as its beat begins and empties as it ends, so the red number is always the
   step the sheet is showing. */
@keyframes rv-step { 0% { opacity: 0; transform: scale(.6); } 6%, 90% { opacity: 1; transform: none; } 100% { opacity: 0; transform: none; } }
