/* ==========================================================================
   The home page's three animated sections, and the product replicas inside
   them.

   Loaded only by landing.html, after app.css and public.css, so every
   colour token, button, panel, tag, queue row and feed row is inherited
   rather than redefined. That inheritance is the point: the screens in
   these sections are supposed to be the product, and the way to keep them
   honest is to make them the same components.

   **The rule this whole file is organised around.** Everything here
   describes the *finished* state of every animation. A section with the
   scripts blocked, or with `prefers-reduced-motion: reduce`, renders
   complete and readable, because complete and readable is the default and
   the scripts have to opt a section into animating by putting `js-motion`
   on <html> and then rewinding it themselves.

   The practical consequence, and the thing to keep true when editing: no
   rule in this file may hide anything, offset anything or scale anything
   unless its selector is inside `.js-motion`. There is a test for it —
   `test_motion.py::test_nothing_is_hidden_without_the_script`.

   No inline styles anywhere, here or in the templates; the CSP allows
   none. GSAP sets `element.style.transform` through the CSSOM, which is a
   different thing and is not what `style-src` governs.

   Only `transform` and `opacity` are animated, everywhere, so every
   animation stays on the compositor. Where something looks like it is
   changing width (the radar sweep, the progress rail) it is a `scaleX` on
   a layer, not a width.
   ========================================================================== */

/* ============================================================== the hero -- */

.hero {
  padding: var(--band-pad) 0;
  border-bottom: 1px solid var(--line-soft);
  position: relative;
  overflow: hidden;
}

.hero-wrap {
  display: grid;
  gap: clamp(28px, 4vw, 52px);
  grid-template-columns: 1fr;
  align-items: center;
}
@media (min-width: 1024px) {
  .hero-wrap { grid-template-columns: 40fr 60fr; }
}

.hero-copy { max-width: 60ch; }

.hero h1 {
  font-size: clamp(2.5rem, 7vw, 4.5rem);
  line-height: 1;
  margin: 0;
  /* Breaks "KEEP THE / WORK COMING." instead of leaving "COMING." alone
     on a line. Progressive enhancement: unsupported browsers get the
     greedy break they always got, which is what this used to be. */
  text-wrap: balance;
  max-width: 14ch;
}

.hero-sub {
  margin: 18px 0 0;
  font-size: clamp(1.0625rem, 1.6vw, 1.3125rem);
  line-height: 1.5;
  color: var(--muted);
  max-width: 34ch;
}

.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin: 28px 0 0; }
.hero-actions .btn { min-height: 48px; padding-inline: 22px; font-size: 1rem; }

/* ------------------------------------------------------------- the map -- */

/* The scene is the frame plus the two cards, which hang over the frame's
   edges. It is not clipped; the frame is. */
.hero-scene {
  position: relative;
  margin: 0;
}

.hero-map { position: relative; }

/* The frame. Everything map-shaped lives in here and is clipped to it,
   so the map fills it edge to edge with no band top or bottom: the SVG
   covers with `preserveAspectRatio: slice` and whatever hangs over the
   long axis is cut off by this. */
.hero-frame {
  position: relative;
  aspect-ratio: 16 / 10;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--panel);
}
@media (min-width: 1024px) {
  .hero-frame { aspect-ratio: 16 / 11; }
}

/* The three layers, stacked and all filling the frame. `slice` on both
   SVGs and the matching transform in radar.js is what keeps the weather
   over the right counties. */
.hero-frame > svg,
.hero-frame > canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* Explicit, because WebKit does not reliably apply the UA
     `overflow: hidden` to an outermost `<svg>` that has been inlined
     into HTML. With `preserveAspectRatio: slice` the paths run past the
     viewBox by design, and on an iPad in landscape that was 450px of
     county outline hanging off the right of the document and a page
     that scrolled sideways. The frame's own `overflow: hidden` clipped
     it visually and did not stop it. */
  overflow: hidden;
}
.hero-radar { pointer-events: none; }

/* ------------------------------------------------------------ the dots -- */

/* Buildings. Stone at low opacity so they read as marks on a map rather
   than as controls, and the ones the storm catches are Orange: a fill,
   not a text colour, which is what D-123 permits. They sit above the
   weather because they are the point of the picture. */
.dot { fill: var(--text); fill-opacity: .62; }
.dot.is-struck { fill: var(--accent); fill-opacity: 1; }

/* ---------------------------------------------------------- the warning -- */

/* A National Weather Service warning polygon the way a radar app draws
   one: a thin yellow-orange outline over the reflectivity, no fill. The
   fill was wrong — it tinted the weather underneath it, which is the one
   thing a warning outline must not do. */
.storm-edge {
  fill: none;
  stroke: #F2B134;
  stroke-width: 3;
  stroke-opacity: .95;
  stroke-linejoin: round;
}

/* ----------------------------------------------------------- the chips -- */

/* Stone on a translucent Charcoal chip, which is the pairing every panel
   on the site already uses: 11.62:1 at full opacity and comfortably over
   4.5:1 at this one, whatever the radar puts underneath. */
.radar-chip {
  position: absolute;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 9px;
  border-radius: var(--radius);
  background: rgba(31, 42, 51, .86);
  border: 1px solid var(--line-strong);
  color: var(--text);
  font-size: .6875rem;
  letter-spacing: .04em;
  line-height: 1;
  white-space: nowrap;
}
.radar-mode { left: 10px; top: 10px; text-transform: uppercase; }
.radar-clock { color: var(--muted); letter-spacing: .02em; }

.radar-key { right: 10px; bottom: 10px; }
.radar-key-n { color: var(--muted); font-size: .625rem; }

/* The dBZ ramp, as a background rather than seven elements. Decorative:
   it is a key to colours that are themselves decorative, and the two
   numbers beside it are the readable part. */
.radar-key-ramp {
  display: block;
  width: 72px;
  height: 7px;
  border-radius: 2px;
  background: linear-gradient(
    to right,
    #9BE08C 0%, #49C03C 20%, #1F8A1F 38%,
    #F2E63D 56%, #F0A028 72%, #DC2F2F 88%, #C026A8 100%
  );
}
@media (max-width: 560px) {
  .radar-key-ramp { width: 52px; }
}

/* ----------------------------------------------------------- the cards -- */

.hero-card {
  position: absolute;
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 11px 13px;
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, .85);
  font-size: .8125rem;
  line-height: 1.45;
  /* 300px, which is the brief, and it is also about the width at which
     this draft wraps to three lines rather than five. */
  width: min(78%, 300px);
  z-index: 2;
}
.hero-card p { margin: 0; }
.hero-card-kind {
  font-size: .6875rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); font-weight: 600; margin-bottom: 5px !important;
}
.hero-card-line { font-weight: 600; }
.hero-card-meta { color: var(--muted); font-size: .75rem; margin-top: 3px !important; }

.hero-card-dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); margin-right: 5px; vertical-align: baseline;
}

/* Hanging over the frame's edges, so neither covers the storm: the alert
   over the top edge on the left, the draft over the bottom-right corner.
   Both are partly outside the frame, which is why `.hero-scene` is not
   clipped and `.hero-frame` is. */
.hero-alert {
  left: clamp(8px, 3%, 28px);
  top: -14px;
  border-left: 3px solid var(--accent);
}
.hero-draft {
  right: clamp(-28px, -2%, 0px);
  bottom: -22px;
}

.hero-card-body {
  margin-top: 7px !important;
  color: var(--muted);
  /* The draft finishes its sentence. `overflow-wrap: break-word` is
     deliberately *not* here: breaking a word mid-character is the thing
     the brief rules out, and nothing in this copy is longer than the
     card. `min-height` reserves the finished box so typing it does not
     resize the card. */
  min-height: 4.4em;
}

.hero-card-actions { margin-top: 9px !important; display: flex; gap: 6px; }

/* A button-shaped thing that is a picture of a button. Not a <button>:
   there is nothing here to press, and a control that does nothing is worse
   than an image of one. Hidden from the accessibility tree by the parent's
   aria-hidden; the card's text says what the product would do. */
.hero-fake-btn,
.fake-btn {
  display: inline-block;
  font-size: .6875rem; font-weight: 600;
  padding: 4px 10px; border-radius: var(--radius);
  border: 1px solid var(--control-line); color: var(--text);
}
.hero-fake-btn.is-primary,
.fake-btn.is-primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.fake-btn.is-done { background: var(--good); color: #0C1A15; border-color: var(--good); }

@media (max-width: 760px) {
  /* Two cards over a 360px map are two unreadable cards. They stack
     under it instead, full width, and the map keeps its own box. */
  .hero-card {
    position: relative;
    width: auto;
    inset: auto;
    margin-top: 10px;
    z-index: auto;
  }
  .hero-alert { border-left: 3px solid var(--accent); }
}

/* ============================================================== the desk -- */

.desk-stage {
  display: grid;
  gap: clamp(20px, 3vw, 36px);
  grid-template-columns: 1fr;
  /* The full-width ending slides the panel in from the right, which for
     half a second puts a 1200px panel 450px past the edge of the
     column. Unclipped that grew the document and gave an iPad in
     landscape a page that scrolled sideways — found in WebKit, not in
     Chromium, because it only appears once the scrub has run.

     `clip` rather than `hidden`: `overflow-x: hidden` forces the other
     axis to `auto`, which would make this a scroll container and is not
     something to do to an element inside a pin. `clip` leaves
     `overflow-y: visible` alone, which is the whole reason it exists. */
  overflow-x: clip;
}
@supports not (overflow-x: clip) {
  .desk-stage { overflow: hidden; }
}
@media (min-width: 1024px) {
  .desk-stage {
    /* The panel is the thing being argued for, so it gets the larger
       half. At 1200px that is 640px of product beside a 520px desk,
       which is enough for the rows to show a title and a building
       without truncating either. */
    grid-template-columns: 0.86fr 1.14fr;
    align-items: start;
  }
  .desk-scene { grid-column: 1; grid-row: 1; }
  .desk-panel { grid-column: 2; grid-row: 1; }

  /* The ending. Once the last thing has flown off the desk, the desk
     has nothing left to say, so it goes and the panel takes the whole
     column: the section finishes on one clean screen of the product
     rather than on a panel beside an empty rectangle.

     `desk.js` adds this class at the end of the timeline and takes it
     off on the way back up. The layout change itself is instant — one
     reflow, not one per frame — and the slide the eye sees is a
     translate the script measures and animates afterwards, which is why
     nothing here transitions. See the note in desk.js.

     The desk keeps its grid cell (`visibility`, not `display`) so that
     removing the class puts it back exactly where it was. */
  /* The pinned section fills the viewport and centres what is in it, so
     the counter and the caption under the panel are on screen for the
     whole scrub rather than below the fold. Without this the section is
     taller than an 800px laptop and the one piece of state the visitor
     needs — how far through they are — is the piece they cannot see.

     The band's own padding shrinks to match: it is sized for a section
     in a scrolling page, not for one holding still and being read. */
  .js-motion .desk {
    min-height: 100vh;
    display: flex;
    align-items: center;
    padding-block: clamp(20px, 3.5vh, 56px);
  }
  .js-motion .desk > .wrap { width: 100%; }

  /* And the two halves are capped so they fit beside the foot. The desk
     keeps its square; the panel scrolls nothing and simply ends. */
  .js-motion .desk-scene { max-height: 52vh; }
  /* Clipped to fit, with the last rows fading out rather than being cut
     through the middle. A row sliced in half reads as a rendering fault;
     a fade reads as a list that continues, which is what it is. Both
     prefixes, because Safari still wants `-webkit-mask-image`. */
  .js-motion .desk-panel {
    max-height: 52vh;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, #000 86%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 86%, transparent 100%);
  }
  .js-motion .desk-stage.is-wide .desk-panel { max-height: 58vh; }

  .desk-stage.is-wide { grid-template-columns: 1fr; }
  .desk-stage.is-wide .desk-scene {
    visibility: hidden;
    /* Out of flow, so the one remaining column is the panel's. */
    position: absolute;
    width: 50%;
  }
  .desk-stage.is-wide .desk-panel { grid-column: 1; grid-row: 1; }
}

/* ------------------------------------------------------------ the desk -- */

.desk-scene {
  position: relative;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* The desk surface. One notch darker than the page so the paper on it
     has something to sit on. */
  background: var(--bg);
  overflow: hidden;
}

/* Except while things are flying off it. The items travel from here to
   the panel, and clipped at the desk's own edge the whole flight
   happened inside the box: an item shrank and faded where it lay
   instead of crossing to the thing it becomes, which is the one moment
   the section exists to show. The stage above is what stops it
   overflowing the page. */
.js-motion .desk-scene { overflow: visible; }

.desk-item { position: absolute; }

/* A coffee ring. The only warm mark on the page that is not the brand
   colour doing a job, and it is at 13% so it reads as a stain. */
.desk-ring {
  position: absolute;
  right: 6%; bottom: 4%;
  width: 17%; aspect-ratio: 1;
  border: 5px solid rgba(211, 84, 31, .13);
  border-radius: 50%;
}

/* A pen, lying across the corner of the desk. Two elements would be
   tidier; it is one rotated rectangle with a darker nib drawn by a
   border, because at this size that is all anybody reads. */
.desk-pen {
  position: absolute;
  /* In the one band the notes and the spreadsheet both leave clear.
     Everything on this desk overlaps something, which is the point, but
     an item that is entirely behind another item is not chaos, it is a
     thing nobody drew. */
  left: 5%; bottom: 40%;
  width: 22%; height: 6px;
  border-radius: 3px;
  background: var(--control-line);
  border-right: 10px solid var(--line-strong);
  transform: rotate(-24deg);
  transform-origin: left center;
}

/* Sticky notes. Stone paper with Charcoal ink, 11.6:1, and the storm one
   on Orange with black ink, 5.06:1 — the two combinations D-126 allows.

   Seven of them, overlapping, at seven different angles. That is the
   point of this section: not "here are some notes" but "this is what
   the top of your desk looks like by Thursday". The first version had
   three, neatly spaced, and read as an illustration of tidiness. */
.desk-note {
  width: 27%;
  aspect-ratio: 1;
  display: flex; align-items: center;
  padding: 8% 7%;
  font-size: clamp(.625rem, 1.35vw, .8125rem);
  line-height: 1.25;
  font-weight: 600;
  background: var(--text);
  color: var(--panel);
  box-shadow: 0 10px 22px -12px rgba(0, 0, 0, .9);
}
.desk-note.is-accent { background: var(--accent); color: var(--accent-ink); }

/* Positions and angles, written out. Seven `nth-child` expressions with
   arithmetic in them would be shorter and nobody could move one note
   without recomputing the others. The stacking order is source order,
   and the template emits them in the order of `NOTES` in
   app/marketing_demo.py. */
.desk-note-1 { left:  2%; top:  4%; transform: rotate(-6deg); }
.desk-note-2 { left: 25%; top:  2%; transform: rotate(4deg); }
.desk-note-3 { left: 48%; top:  7%; transform: rotate(-3deg); }
.desk-note-4 { left: 14%; top: 24%; transform: rotate(7deg); }
.desk-note-5 { left: 38%; top: 28%; transform: rotate(-9deg); }
.desk-note-6 { left: 60%; top: 31%; transform: rotate(5deg); }
.desk-note-7 { right: 3%; top:  3%; transform: rotate(9deg); }

/* The printed spreadsheet, half under the notes. Skewed and rotated so
   it reads as a sheet thrown down rather than a table rendered. */
.desk-paper {
  left: 3%; bottom: 4%;
  width: 46%;
  padding: 3.5% 4%;
  background: #D8D5CF;
  transform: rotate(-4deg) skewX(-3deg);
  box-shadow: 0 14px 30px -16px rgba(0, 0, 0, .9);
}
.desk-paper-head,
.desk-paper-row {
  display: block;
  height: 4px;
  background: #8E8B86;
  margin-bottom: 6px;
}
.desk-paper-head { height: 7px; background: #5E5B57; width: 52%; }
.desk-paper-row:nth-child(odd) { width: 94%; }
.desk-paper-row:nth-child(even) { width: 78%; }
.desk-paper-label {
  display: block;
  margin-top: 7px;
  font-size: .5625rem;
  letter-spacing: .02em;
  color: #5E5B57;
}

/* The fanned stack of cards. */
.desk-cards { right: 4%; bottom: 20%; width: 29%; aspect-ratio: 1.75; }
.desk-card {
  position: absolute; inset: 0;
  background: #D8D5CF;
  border-radius: 2px;
  box-shadow: 0 8px 18px -10px rgba(0, 0, 0, .9);
  padding: 14% 12%;
}
.desk-card:nth-child(1) { transform: rotate(-15deg); }
.desk-card:nth-child(2) { transform: rotate(-6deg); }
.desk-card:nth-child(3) { transform: rotate(3deg); }
.desk-card:nth-child(4) { transform: rotate(12deg); }
.desk-card-rule { display: block; height: 4px; width: 62%; background: #5E5B57; margin-bottom: 6px; }
.desk-card-rule.is-short { width: 40%; background: #8E8B86; }

/* The three notifications, as lock-screen banners.

   These replaced a phone with "3 missed calls" on it. The number was the
   wrong thing to draw: it said how many and not what they were, and what
   they were is the argument. A call, an email and a text, each from
   somebody with a name, each about a building.

   Stone with Charcoal ink, the same pairing as the notes and the same
   11.6:1, because the banner carries real sentences and they have to be
   readable at 12px on a dark desk. Rounded hard, which is the one thing
   that makes a rectangle read as a phone notification rather than as
   another piece of paper. */
.desk-banner {
  width: 38%;
  display: flex;
  gap: 7px;
  align-items: flex-start;
  padding: 3.4% 4%;
  border-radius: 13px;
  background: var(--text);
  color: var(--panel);
  box-shadow: 0 12px 26px -14px rgba(0, 0, 0, .9);
}

/* Stacked in the band the spreadsheet and the cards leave, newest at the
   top, each one a little straighter than the one above it. Written out
   for the same reason the notes' positions are: three `nth-child`
   expressions with arithmetic in them would be shorter and nobody could
   move one without recomputing the others. */
.desk-banner-1 { left: 41%; bottom: 21%; transform: rotate(-6deg); }
.desk-banner-2 { left: 45%; bottom: 11%; transform: rotate(-3deg); }
.desk-banner-3 { left: 49%; bottom:  1%; transform: rotate(-1deg); }

.desk-banner-badge {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  border-radius: 6px;
  /* Charcoal on each of the three, which is 7:1 or better on all of
     them. The icon is the only thing in the badge, so this is a
     non-text contrast and the 3:1 floor would have allowed much less. */
  color: var(--bg);
}
/* Three of the brand's own colours, one per kind. Nothing here is
   anybody else's icon or anybody else's green. */
.desk-banner-badge.is-call { background: var(--good); }
.desk-banner-badge.is-mail { background: var(--control-line-hover); }
.desk-banner-badge.is-text { background: var(--accent); }

.notice-icon {
  width: 14px; height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The handset is a solid shape, not an outline: two small quadrilaterals
   stroked at 1.9 would close up into blobs at 14px. */
.desk-banner-badge.is-call .notice-icon path:not(:last-child) {
  fill: currentColor;
}

.desk-banner-text { min-width: 0; display: block; }
.desk-banner-head {
  display: flex; gap: 6px; align-items: baseline;
  justify-content: space-between;
}
.desk-banner-kind {
  font-size: clamp(.5rem, 1vw, .625rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
}
.desk-banner-when {
  font-size: clamp(.4375rem, .9vw, .5625rem);
  /* Charcoal at 70%, which is 6.3:1 on Stone. The timestamp is the one
     thing on a notification nobody reads first. */
  color: rgba(31, 42, 51, .72);
  white-space: nowrap;
}
.desk-banner-who {
  display: block;
  margin-top: 1px;
  font-size: clamp(.5625rem, 1.2vw, .75rem);
  font-weight: 600;
  line-height: 1.2;
}
.desk-banner-line {
  font-size: clamp(.5rem, 1.1vw, .6875rem);
  line-height: 1.25;
  /* Two lines at most. A banner on a phone truncates, and one here that
     wrapped to four lines would read as a card rather than as a
     notification. `-webkit-box` is the only display value line-clamp
     works with, and it is honoured in every engine this site supports. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* ----------------------------------------------------------- the panel -- */

/* The panel's rows run at the application's own type size. They used to
   inherit the replica scale, which inside a half-column came out at
   12px, and a marketing page that asks somebody to read a product screen
   has to let them. */
.desk-rows { padding: 12px 14px 14px; }
.desk-panel .queue-row { font-size: .875rem; }
.desk-panel .feed-row { font-size: .875rem; }
.desk-panel .queue-touch { font-size: .8125rem; }
.desk-group + .desk-group { margin-top: 16px; }
.desk-group-head {
  margin: 0 0 4px;
  font-size: .6875rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); font-weight: 600;
}

/* ----------------------------------------------- the foot of the section -- */

/* Under the panel: a thin progress bar, the counter, and one caption
   line that changes with the scrub. This replaced a list of five
   checkmarks that said all five things at once, which meant it said none
   of them at the moment the matching item was actually flying. */
.desk-foot {
  margin-top: clamp(18px, 2.4vw, 28px);
  display: grid;
  gap: 10px;
}
@media (min-width: 1024px) {
  .desk-foot { grid-column: 1 / -1; }
}

/* A `scaleX` on a full-width layer, not a width, so the bar costs the
   compositor a transform per frame and nothing a layout. */
.desk-progress {
  height: 2px;
  background: var(--line);
  border-radius: 1px;
  overflow: hidden;
}
.desk-progress-fill {
  display: block;
  height: 100%;
  background: var(--accent);
  transform: scaleX(1);
  transform-origin: left center;
}

.desk-count {
  margin: 0;
  font-size: .8125rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.desk-count [data-desk-done] { color: var(--text); font-weight: 700; }

/* The captions are stacked in one cell and cross-faded, so the line
   never changes height as the words change and nothing below it moves.
   `grid` rather than absolute positioning, so the tallest caption still
   sizes the box and a long one is never clipped. */
/* With no script running these are a list: ten short lines that say
   what the panel beside them is, which is complete and readable and is
   what the old checkmark list did. The *stacking* is the animated
   behaviour and lives under `.js-motion` below, so nothing here hides
   anything a script has to come back and reveal. */
.desk-caption-line { margin: 0; display: grid; gap: 6px; }
.desk-caption {
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.45;
  max-width: 62ch;
}

/* ============================================================= the story -- */

.story-rail { margin: 0 0 clamp(20px, 3vw, 32px); }
.story-rail ol {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 4px;
  grid-template-columns: 1fr;
}
@media (min-width: 760px) {
  .story-rail ol { grid-template-columns: repeat(4, 1fr); gap: 0 14px; }
}
.story-rail a {
  display: flex; align-items: center; gap: 9px;
  min-height: var(--tap);
  padding: 8px 2px;
  font-size: .9375rem;
  color: var(--muted);
  text-decoration: none;
  /* The rail's own rule. A 2px line that the active step fills, drawn as a
     pseudo-element so the fill is a scaleX rather than a width. */
  border-top: 2px solid var(--line);
  position: relative;
}
.story-rail a::after {
  content: "";
  position: absolute; left: 0; right: 0; top: -2px;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
}
.story-rail a:hover { color: var(--text); }
.story-rail a:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.story-rail a span {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; flex: none;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  font-size: .6875rem; font-weight: 700;
}
.story-rail a.is-current { color: var(--text); }
.story-rail a.is-current::after { transform: scaleX(1); }
.story-rail a.is-current span { border-color: var(--accent); }

/* ------------------------------------------------------------- the grid -- */

.story-grid {
  display: grid;
  gap: clamp(20px, 3vw, 40px);
  grid-template-columns: 1fr;
}

.story-copy { max-width: 52ch; }
/* `:focus-visible` only. The first version also had a blanket
   `:focus { outline: none }`, on the theory that a `tabindex="-1"`
   element taking programmatic focus from the rail should not flash a
   ring. Every engine that matters already draws no ring for programmatic
   focus, so the rule bought nothing and was the exact shape of the
   accessibility bug the suite greps for. */
.story-copy:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 6px; }
.story-n {
  margin: 0 0 6px;
  font-size: .75rem; text-transform: uppercase; letter-spacing: .1em;
  color: var(--muted); font-weight: 600;
}
.story-copy h3 { margin: 0 0 10px; }
.story-copy p { margin: 0; color: var(--muted); line-height: 1.65; }

.story-screen + .story-copy { margin-top: clamp(16px, 3vw, 32px); }

@media (min-width: 1024px) {
  /* Four rows, screen on the left and its text on the right. This is the
     layout with the scripts off and under `prefers-reduced-motion`: four
     steps, all four readable, in order, which is what "show the final
     state of every animation" means for a section whose animation is
     *which step you are looking at*.

     The stacked, pinned version is below, inside `.js-motion`. Putting
     the stack here instead was the first version and it was wrong: with
     reduced motion on, all four screens and all four blocks of text sat
     on top of each other at full opacity and the section was unreadable.
     That is the failure this file's header is about. */
  /* 60 / 40. The screen is the argument and the words are the caption;
     the first version split it 54/46 and the product window came out
     556px wide inside a 1088px column, which is a thumbnail of a
     1400px screen and unreadable at a glance. */
  .story-grid {
    grid-template-columns: 64fr 36fr;
    align-items: center;
    gap: clamp(36px, 5vw, 72px) clamp(32px, 4vw, 48px);
  }
  .story-screen { grid-column: 1; }
  .story-copy { grid-column: 2; align-self: center; }
  .story-screen + .story-copy { margin-top: 0; }
  /* Row N holds step N's pair. Written out rather than generated,
     because there are four of them and `nth-child` arithmetic over two
     interleaved element types is the kind of selector that breaks the
     day somebody adds a fifth step. */
  .story-copy[data-step="0"], .story-screen[data-step="0"] { grid-row: 1; }
  .story-copy[data-step="1"], .story-screen[data-step="1"] { grid-row: 2; }
  .story-copy[data-step="2"], .story-screen[data-step="2"] { grid-row: 3; }
  .story-copy[data-step="3"], .story-screen[data-step="3"] { grid-row: 4; }
}

/* ===================================== the product replicas, shared bits -- */

.appwin { width: 100%; }
.appwin .frame {
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--panel);
  overflow: hidden;
  box-shadow: 0 24px 60px -28px rgba(0, 0, 0, .75);
}

.appwin-body { background: var(--bg); }

/* The product's top bar, at the size a window this wide would show it. */
.appbar {
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap;
  padding: 7px 11px;
  background: var(--panel);
  border-bottom: 1px solid var(--line-strong);
}
/* Two class names, so this beats `.frame img` in public.css, which sets
   `width: 100%` for the product screenshots that live in the same kind of
   frame. One class loses the specificity race and the lockup renders at
   the full width of the window, which is how this was found. */
.appbar .appbar-logo {
  height: 15px;
  width: auto;
  display: block;
  flex: none;
}
.appbar-tenant {
  font-size: .6875rem; font-weight: 600; color: var(--text);
  white-space: nowrap;
}
.appbar-nav { display: flex; gap: 2px; flex-wrap: wrap; }
.appbar-link {
  font-size: .625rem;
  padding: 3px 6px;
  border-radius: 2px;
  color: var(--muted);
  white-space: nowrap;
}
.appbar-link.is-active { color: var(--accent-ink); background: var(--accent); }

.appwin-page { padding: 12px 14px 14px; }
.appwin-page.is-standalone { background: var(--bg); padding: 0; }

.appwin-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; margin-bottom: 8px;
  padding-bottom: 7px; border-bottom: 1px solid var(--line-soft);
}
.appwin-head h3 {
  margin: 0;
  font-family: "Barlow Condensed", var(--font);
  font-style: italic; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0;
  font-size: 1rem;
}
.appwin-sort, .appwin-subhead {
  font-size: .6875rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em; font-weight: 600;
}
.appwin-subhead { display: block; margin: 14px 0 2px; }

/* The count the product's list screens carry under a table. Real, and
   the last few pixels that make the four story screens one height. */
.appwin-foot {
  margin: 10px 0 0;
  padding-top: 8px;
  border-top: 1px solid var(--line-soft);
  font-size: .75rem;
  color: var(--muted);
}

/* **The replicas run at the application's own type size, not smaller.**

   They used to run at about 80%, on the reasoning that the window is a
   picture of a 1400px screen inside a 600px column. The reasoning was
   wrong: nobody reading a marketing page is comparing it to a 1400px
   screen, they are reading the words, and 12px rows inside a frame are
   a picture of software rather than software. These are the values from
   app.css — `.queue-row` and `.feed-row` are both .875rem there — so a
   row here is at least as legible as the same row in the product at
   100% zoom.

   Everything else about the rows is still app.css's: the separators,
   the flex, the ellipsis on a long building name. */
.appwin .queue-row { padding: 8px 0; font-size: .875rem; }
.appwin .queue-touch { font-size: .8125rem; }
.appwin .feed-row {
  padding: 7px 0; font-size: .875rem;
  grid-template-columns: 62px minmax(0, 1fr) auto;
}
.appwin .tag { font-size: .6875rem; padding: 2px 6px; }

/* The job title, between the name and the building. New here rather
   than in app.css: the product puts a contact's title on their own
   record and not in the queue, and a marketing replica that invented a
   column the product does not have would be the one place these screens
   lie. It is the same `.card-sub` treatment the product uses for a
   secondary line, so it reads as part of the same system — and it is
   the first thing dropped when the row runs out of room. */
.queue-role {
  color: var(--muted);
  font-size: .8125rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
@media (max-width: 1100px) {
  /* Four columns of text in a window half a laptop wide is three of
     them truncated. The building is the one that identifies the row. */
  .appwin .queue-role { display: none; }
}

/* Same reasoning, by layout rather than by viewport. The desk panel is
   half the content column until the section's ending widens it, so the
   title is dropped while it is narrow and comes back with the room.
   A container query would express this directly; this is two selectors
   and works in the browsers the product already supports. */
.desk-panel .queue-role { display: none; }
.desk-stage.is-wide .desk-panel .queue-role { display: block; }

.tag-new { color: var(--text); border-color: var(--line-strong); }

/* The highlight on "the person nobody has called". A tinted layer behind
   the row at zero opacity, faded in by a class rather than by tweening
   `background-color`: a colour tween is a paint on every frame, and this
   is the one step with six rows moving at once. The tint is the brand
   colour as a background, which is what D-123 allows it to be; the row's
   own text is unchanged and still 11.6:1. */
.appwin .queue-row { position: relative; }
.appwin .queue-row::after {
  content: "";
  position: absolute;
  inset: 0 -8px;
  background: var(--accent);
  opacity: 0;
  transition: opacity .45s ease;
  pointer-events: none;
}
.appwin .queue-row.is-top::after { opacity: .14; }
/* Everything in the row has to sit over the tint. */
.appwin .queue-row > * { position: relative; }

@media (prefers-reduced-motion: reduce) {
  .appwin .queue-row::after { transition: none; }
}

/* ---------------------------------------------------------- the draft -- */

.draft {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 11px 12px;
  position: relative;
}
.draft p { margin: 0; }
.draft-why {
  font-size: .6875rem; line-height: 1.45; color: var(--muted);
  padding-bottom: 8px; margin-bottom: 8px !important;
  border-bottom: 1px solid var(--line-soft);
}
.draft-why .tag { margin-right: 6px; }
.draft-to { font-size: .75rem; margin-bottom: 7px !important; }
.draft-to .muted { margin-left: 7px; }
.draft-body {
  font-size: .75rem; line-height: 1.55; color: var(--muted);
  /* Reserved for the finished text, so typing it does not resize the card.
     Four lines at this column width. */
  min-height: 4.65em;
}
.draft-actions { margin-top: 10px !important; display: flex; gap: 6px; }

/* The cursor that presses Approve. A pointer drawn in CSS rather than an
   image: two borders and a rotation, which is a shape the compositor can
   move without repainting. */
.fake-cursor {
  position: absolute;
  left: 52px; bottom: 16px;
  width: 0; height: 0;
  border-left: 7px solid var(--text);
  border-right: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-top: 11px solid var(--text);
  transform: rotate(-20deg);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .9));
}

/* ============================================== scan the card. done. -- */

/* The one section on this page that shows a thing somebody does with
   their hands: a phone held over a business card, and the five fields it
   becomes.

   Everything here is the finished state. The phone shows the card in the
   frame, the five fields are filled, the saved contact sits below the
   viewfinder and the contact list has the new row. `cardstory.js` rewinds
   it; the rules that do the hiding are in the motion layer at the bottom
   of this file. */

.card-stage { display: grid; gap: clamp(20px, 3vw, 32px); }

@media (min-width: 900px) {
  .card-stage {
    grid-template-columns: minmax(0, 290px) minmax(0, 1fr);
    gap: clamp(28px, 4vw, 56px);
    align-items: start;
  }
}

.card-phone-col { display: flex; justify-content: center; }

/* The handset. Two rounded rectangles and a speaker bar: enough to read
   as a phone, and nothing that reads as a particular phone. */
.card-phone {
  width: clamp(208px, 70vw, 272px);
  padding: 9px;
  border: 3px solid var(--control-line);
  border-radius: 28px;
  background: var(--panel-2);
  box-shadow: 0 18px 40px -22px rgba(0, 0, 0, .95);
}
.card-phone-bar {
  display: block;
  width: 34%; height: 4px;
  margin: 1px auto 7px;
  border-radius: 2px;
  background: var(--line-strong);
}
.card-screen {
  position: relative;
  overflow: hidden;
  border-radius: 20px;
  background: var(--bg);
}

/* The viewfinder. Four corner marks, which is how every camera says
   "this part", and a shutter button under them. */
.card-finder {
  position: relative;
  display: grid;
  place-items: center;
  /* A phone's shape, not the card's. Sized from a ratio rather than from
     its contents, because a viewfinder that is only as tall as the thing
     in it reads as a tablet: the first version of this was 254 by 150
     and looked like an iPad on its side. The saved contact is parked
     over exactly this box, so this is also what sets how far it slides. */
  aspect-ratio: 9 / 13;
  padding: 26px 16px 58px;
  background: var(--panel);
}
.card-corner {
  position: absolute;
  width: 17px; height: 17px;
  border: 2px solid var(--accent);
}
.card-corner.c1 { top: 14px; left: 12px; border-right: 0; border-bottom: 0; }
.card-corner.c2 { top: 14px; right: 12px; border-left: 0; border-bottom: 0; }
.card-corner.c3 { bottom: 48px; left: 12px; border-right: 0; border-top: 0; }
.card-corner.c4 { bottom: 48px; right: 12px; border-left: 0; border-top: 0; }

/* The card on the table. Stone with Charcoal ink, the same pairing as
   the desk's notes, because the whole point is that it is readable. */
.card-paper {
  width: 100%;
  padding: 11px 12px 10px;
  border-radius: 2px;
  background: var(--text);
  color: var(--panel);
  transform: rotate(-1.6deg);
  box-shadow: 0 10px 22px -12px rgba(0, 0, 0, .9);
}
.card-paper p { margin: 0; }
.card-paper-name { font-size: .875rem; font-weight: 700; line-height: 1.15; }
.card-paper-title,
.card-paper-company {
  font-size: .6875rem;
  line-height: 1.3;
  /* Charcoal at 72%, 6.3:1 on Stone. Printed small, like it is on a
     card, and still above the floor. */
  color: rgba(31, 42, 51, .78);
}
.card-paper-company { font-weight: 600; color: var(--panel); }
.card-paper-lines {
  display: flex;
  gap: 10px;
  margin-top: 7px;
  font-family: var(--mono);
  font-size: .5625rem;
  color: rgba(31, 42, 51, .78);
}

/* The shutter. Centred with auto margins rather than a translate,
   because a transform here is a transform the script would overwrite. */
.card-shutter {
  position: absolute;
  bottom: 13px; left: 0; right: 0;
  width: 32px; height: 32px;
  margin-inline: auto;
  border: 2px solid var(--text);
  border-radius: 50%;
}
.card-shutter::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--text);
}

/* The capture.

   An empty `::after` on the screen rather than an element in the markup,
   and its opacity comes from a custom property the script writes. Two
   reasons, and the second is the real one:

   * there is nothing in a flash for a reader to miss, so it has no
     business being in the HTML at all; and
   * a `<span>` sitting at `opacity: 0` in the finished stylesheet is a
     rule that hides something for every visitor whose script did not
     run, which is the one thing motion.css is not allowed to do. A
     decorative layer that is invisible *at rest* is a different claim
     from a section that is invisible *until a script arrives*, and
     keeping them apart is what `test_nothing_is_hidden_without_the_script`
     exists for.

   `--flash` is the only custom property any of these scripts animates.
   It drives one opacity on one compositor layer for about half a
   second. */
.card-screen::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--text);
  opacity: var(--flash, 0);
  pointer-events: none;
}

/* The saved contact. Below the viewfinder in the flow, so with no script
   both are readable; the motion layer lifts it over the screen and slides
   it up. Generic on purpose: a disc of initials, a name, two numbers and
   one button. */
.card-contact {
  padding: 14px 13px 15px;
  background: var(--panel-2);
  border-top: 1px solid var(--line);
}
.card-contact p { margin: 0; }
.card-contact-mono {
  display: grid;
  place-items: center;
  width: 42px; height: 42px;
  margin: 0 auto 9px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: .875rem;
  font-weight: 700;
  letter-spacing: .02em;
}
.card-contact-name {
  font-size: .9375rem; font-weight: 700; text-align: center;
}
.card-contact-sub {
  font-size: .75rem; text-align: center; color: var(--muted);
}
.card-contact-rows {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 10px;
  margin: 11px 0 12px;
  font-size: .75rem;
}
.card-contact-rows dt {
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: .625rem;
  color: var(--muted);
  align-self: center;
}
.card-contact-rows dd { margin: 0; font-family: var(--mono); }

/* The two buttons. Pictures of buttons: a `span`, because there is
   nothing behind either of them and a `<button>` that does nothing is a
   stop on the keyboard path for no reason. */
.card-contact-btn,
.card-save {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 34px;
  padding: 7px 14px;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: .8125rem;
  font-weight: 600;
}
.card-contact-btn { width: 100%; }

/* A tap is drawn as the button pressing: `cardstory.js` punches the
   scale down and lets it back. No ring element and nothing hidden, and a
   button that gives under a thumb reads as a tap more plainly than a
   circle appearing beside one does. */

/* ------------------------------------------------- what it becomes -- */

.card-side { display: grid; gap: 16px; align-content: start; }

.card-review { padding: 14px 16px 16px; }
.card-review-head,
.card-list-head {
  margin: 0 0 8px;
  font-size: .6875rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); font-weight: 600;
}
.card-field-list { list-style: none; margin: 0; padding: 0; display: grid; }
.card-field {
  display: grid;
  grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: 10px;
  align-items: baseline;
  padding: 7px 0;
  border-bottom: 1px solid var(--line-soft);
}
.card-field:last-child { border-bottom: 0; }
.card-field-label {
  font-size: .6875rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted);
}
.card-field-value { font-weight: 600; }
.card-review-note {
  margin: 9px 0 13px;
  font-size: .75rem;
  color: var(--muted);
}

.card-list-rows { padding: 12px 14px 14px; }
.card-list .queue-row { font-size: .875rem; }
.card-list .queue-touch { font-size: .8125rem; }

/* ===================================================== the motion layer -- */

/* Everything below this line is scoped inside `.js-motion`, which
   motion.js adds to <html> only when it has GSAP, has ScrollTrigger, and
   has established that the visitor has not asked for reduced motion. Above
   this line is the finished page; below it is the before.

   If you add a rule here, it must hide or move something. If you add a
   rule that is needed for the page to read correctly, it belongs above. */

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

.js-motion .dot { opacity: 0; }
.js-motion .storm-edge { opacity: 0; }
.js-motion .hero-card { opacity: 0; }

/* ------------------------------------------------------------- the desk -- */

.js-motion .desk-group { opacity: 0; }
/* Every caption but the current one. With the script off none of this
   applies and they would all stack in the one grid cell, so the markup's
   finished state is the *last* caption visible and the rest beneath it;
   `.is-current` is what the script moves. */
/* Stacked into one cell and cross-faded, so the line never changes
   height as the words change and nothing below it moves. `grid` rather
   than absolute positioning, so the tallest caption still sizes the box
   and a long one is never clipped. */
.js-motion .desk-caption-line { gap: 0; min-height: 2.9em; }
.js-motion .desk-caption { grid-area: 1 / 1; opacity: 0; }
.js-motion .desk-caption.is-current { opacity: 1; }
.js-motion .desk-progress-fill { transform: scaleX(0); }

/* ------------------------------------------------------------ the story -- */

/* Only on the widths that pin. Below 1024 the four steps stay four
   stacked blocks and each one animates in place as it enters view.

   This is where the four pairs collapse into one cell. It is inside
   `.js-motion`, so it only ever applies when there is a script running
   that will cross-fade them; see the note on the four-row layout above
   for what happens otherwise. */
@media (min-width: 1024px) {
  .js-motion .story-grid {
    gap: 0 clamp(32px, 4vw, 48px);
    /* The pinned cell. Tall enough to hold the product window and its
       text and no taller: `72vh` left the window floating in the middle
       of a box with a band of dead space between it and the rail above,
       which is what this pass was called to fix. The window is about
       420px at these widths, so the cell is sized from its contents and
       only given a floor.

       `align-content: center` rather than `start`, so what little slack
       there is sits above and below the pair rather than all under it. */
    min-height: 0;
    align-content: center;
  }
  /* The rail is the heading for what is pinned under it, so the gap
     between them is a heading's gap and not a section's. */
  .js-motion .story-rail { margin-bottom: clamp(14px, 1.6vw, 22px); }

  /* The pinned section fills the viewport and centres what is in it.
     Without this the whole block sits against the top of a tall screen
     and leaves a third of a 1080p display empty underneath — the band's
     own padding is sized for a section in a scrolling page, not for one
     that is holding still and being looked at. The padding shrinks to
     match, so a 1280x800 laptop still fits the lot without scrolling
     inside the pin. */
  .js-motion .story {
    min-height: 100vh;
    display: flex;
    align-items: center;
    padding-block: clamp(24px, 4vh, 64px);
  }
  .js-motion .story > .wrap { width: 100%; }

  /* **All four windows the same height, and the height comes from the
     content rather than from a number.**

     The four screens share one grid cell, so the row is as tall as the
     tallest of them. The first version then set a `min-height` on the
     body to match, which equalised the frames but left the shorter
     screens with a band of nothing under their last row — most
     obviously the storm step, which was eighty pixels short of step
     four. A hard-coded height is also wrong the first time somebody
     edits the copy.

     So the frames stretch instead: the grid row sizes to the tallest
     content, and this chain of `flex` hands that height down through
     `.appwin` and `.frame` to `.appwin-body`. Nothing is clipped,
     nothing is padded to a magic number, and the four screens' content
     is balanced in the templates so the slack is about ten pixels
     rather than eighty. A product window that changes size as the
     visitor scrolls between steps is not what an application looks
     like; a window that keeps its shape while the content inside it
     changes is.

     Only inside `.js-motion`: with the scripts off each screen is in
     its own row and a tall one is just a tall one. */
  /* `align-self: stretch` undoes the `align-items: center` the
     four-row layout sets on the grid: with it centred, each screen
     sizes to its own content and the four frames come out 425, 425,
     442 and 433 instead of one height. Stretched, every screen is the
     row's height and the row is the tallest content. */
  .js-motion .story-screen { display: flex; align-self: stretch; }
  .js-motion .story-screen .appwin { display: flex; flex-direction: column; }
  .js-motion .story-screen .frame { flex: 1; display: flex; flex-direction: column; }
  .js-motion .story-screen .appwin-body { flex: 1; }
  .js-motion .story-screen,
  .js-motion .story-copy {
    grid-row: 1;
    opacity: 0;
  }
  /* The first pair is visible from the start, so the section does not
     begin as an empty box the height of a product screen. */
  .js-motion .story-screen[data-step="0"],
  .js-motion .story-copy[data-step="0"] { opacity: 1; }
}

.js-motion .appwin [data-type],
.js-motion .hero-card [data-type] {
  /* The typing animation writes into these. Emptied by the script rather
     than by CSS, because `min-height` above is what holds the box open
     and an emptied element with a reserved height shifts nothing. */
  white-space: pre-wrap;
}

/* --------------------------------------------- scan the card. done. -- */

/* The before. Each rule here hides or moves one beat of the sequence,
   and with no script none of them applies: the card is in the frame, the
   five fields are filled, the saved contact sits below the viewfinder
   and the new row is at the top of the list. */

/* The saved contact leaves the flow and covers the screen, parked one
   screen down. This is the only rule in this section that changes a
   layout rather than an opacity, and it is the reason the markup can
   have both views readable at once. */
.js-motion .card-screen { contain: paint; }
.js-motion .card-contact {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-top: 0;
  transform: translateY(100%);
}
.js-motion .card-field { opacity: 0; }
.js-motion .card-list .queue-row:first-child { opacity: 0; }

/* ------------------------------------------------- the reduced-motion gate -- */

/* Belt to motion.js's braces. The script checks the media query before it
   adds `js-motion` and re-checks when the setting changes, so this should
   never be the thing that saves a visitor. It is here because the cost is
   four lines and the failure it covers — a script that adds the class and
   then throws before it reads the query — is silent and would leave a
   page with holes in it. */
@media (prefers-reduced-motion: reduce) {
  .js-motion .dot,
  .js-motion .storm-edge,
  .js-motion .hero-card,
  .js-motion .desk-group,
  .js-motion .desk-caption.is-current,
  .js-motion .desk-progress-fill,
  .js-motion .story-screen,
  .js-motion .story-copy,
  .js-motion .card-field,
  .js-motion .card-list .queue-row:first-child {
    opacity: 1 !important;
    transform: none !important;
  }
  /* The saved contact goes back under the viewfinder rather than over
     it, which is the markup's own layout. */
  .js-motion .card-contact {
    position: static;
    transform: none !important;
    border-top: 1px solid var(--line);
  }
}
