/* Lifted verbatim from the design canvas helmet. Rebuild with build_pages.mjs. */
@import url("./tokens.css");

/* The one place the header's scroll-reveal wordmark is tuned -- every
       .navwordmark rule below reads these, so a page adding its own copy of
       the navdock (a lander that isn't one of these three artboards) matches
       automatically as long as it loads this stylesheet. */
    :root {
      --nav-wordmark-w: 160px;
      --nav-brand-gap: 8px;
      --nav-wordmark-in: 260ms;
      --nav-wordmark-fade: 200ms;
    }
    body { margin: 0; font-family: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: var(--color-body); background-color: var(--color-page); }
    /* Matches the app's --color-selection (the accent, mixed low so it reads as a neutral grey band).
       No color override: forcing dark text here read fine on light sections but
       put dark text on the tint over dark cards, buttons and panels too, which
       is unreadable. Leaving color unset keeps each element's own
       already-contrast-correct text color under the highlight. */
    ::selection { background-color: color-mix(in srgb, var(--color-accent) 28%, transparent); }
    ::-moz-selection { background-color: color-mix(in srgb, var(--color-accent) 28%, transparent); }
    * { box-sizing: border-box; }
    a { color: inherit; text-decoration: none; cursor: pointer; }
    a:hover { color: var(--color-ink); }
    .serif { font-family: 'Playfair Display', Georgia, 'Times New Roman', serif; color: var(--color-ink); font-weight: 600; letter-spacing: -0.025em; margin: 0; }
    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace; }
    .eyebrow { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 10px; letter-spacing: 0.3em; text-transform: uppercase; color: var(--color-faint); margin: 0; }
    .cta { display: inline-flex; align-items: center; justify-content: center; gap: 10px; background-color: var(--color-ink); color: var(--color-page); border: 1px solid var(--color-ink); border-radius: 9999px; font-family: inherit; font-weight: 500; font-size: 16px; padding: 14px 32px; cursor: pointer; }
    .cta:hover { background-color: var(--color-body); border-color: var(--color-body); color: var(--color-page); }
    .cta-sm { font-size: 13px; padding: 8px 16px; }
    .quiet { font-size: 14px; color: var(--color-faint); }
    .quiet:hover { color: var(--color-ink); }
    .navlink { position: relative; font-size: 12px; letter-spacing: 0.025em; color: var(--color-faint); padding-bottom: 2px; transition: color 200ms ease; }
    .navlink:hover { color: var(--color-ink); }
    /* Two origins, not one: the bar wipes in from the left and leaves to the
       right, so the hover reads as a stroke drawn under the word rather than
       one that retracts back along the path it came in on. */
    .navlink::after {
      content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
      background-color: currentColor; transform: scaleX(0); transform-origin: right;
      transition: transform 240ms cubic-bezier(0.4, 0, 0.2, 1);
    }
    .navlink:hover::after, .navlink[aria-current]::after { transform: scaleX(1); transform-origin: left; }
    .card { border: 1px solid var(--color-hairline); border-radius: 16px; background-color: var(--color-white); box-shadow: 0 1px 3px rgba(31,29,26,0.06); }
    .rule { height: 1px; background-color: var(--color-hairline); }
    /* The showcase card's band bar, following the app's own MetricRow
       (App/src/app/components/results/MetricRow.tsx): the dot is the average
       reading, the shaded run is the middle 50%, and each names itself on
       hover. The band's glow is on the fill rather than on .mband, because
       showcase.js drives .mband's own transition to fade the band in. */
    .mband { position: absolute; top: 0; height: 100%; z-index: 1; }
    .mband-fill { position: absolute; inset: 0; background-color: rgba(0,0,0,0.15); transition: box-shadow 200ms ease; }
    .mband:hover .mband-fill { box-shadow: 0 0 6px 1px rgba(0,0,0,0.28); }
    .mpoint { position: absolute; top: 50%; width: 16px; height: 16px; margin: -8px 0 0 -8px; z-index: 2; }
    .mglow { position: absolute; inset: -4px; border-radius: 9999px; opacity: 0; filter: blur(4px); transition: opacity 200ms ease; pointer-events: none; }
    .mdot { position: absolute; inset: 0; border-radius: 9999px; border: 2px solid var(--color-white); box-shadow: 0 2px 8px rgba(31,29,26,0.20); transition: transform 200ms ease; }
    .mpoint:hover .mglow { opacity: 0.6; }
    .mpoint:hover .mdot { transform: scale(1.1); }
    .mtip { position: absolute; bottom: 100%; left: 50%; margin-bottom: 4px; transform: translateX(-50%); white-space: nowrap; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-ink); opacity: 0; transition: opacity 150ms ease; pointer-events: none; }
    .mtip-band { margin-bottom: 6px; }
    .mpoint:hover .mtip, .mband:hover .mtip { opacity: 1; }
  
    /* ── responsive ──────────────────────────────────────
       Layout reflows on the room a row actually has, not on the
       viewport width: an artboard can be any width, so a width
       breakpoint stacks columns that still had space for each other.
       Sizes are fluid rather than stepped, and the display scale sits
       roughly 10% under the 1440 draft, which read oversized on a
       real screen. */
    /* The artboard is 1440 wide; the page it becomes is not. Capping the page
       there leaves the body colour showing as two bars on anything wider, so
       the page runs the full viewport and the gutter holds the content in
       instead. */
    .page { width: 100% !important; max-width: none !important; margin: 0 auto; }
    img, svg { max-width: 100%; }
    select, input { max-width: 100%; }

    /* Gutters track width -- 6.7vw meets the authored 96px at 1440 -- until
       past 1440, where they take over the centring and hold the content box
       at the 1248 it was drawn at. max() picks whichever is wider, so the two
       regimes meet exactly at 1440 with no step, and every section's sky and
       the dark band still reach both edges. */
    .page > div:not(.navdock) {
      padding-left: max(clamp(20px, 6.7vw, 96px), calc((100% - 1248px) / 2)) !important;
      padding-right: max(clamp(20px, 6.7vw, 96px), calc((100% - 1248px) / 2)) !important;
    }
    /* The dock rides the viewport; the pill it sits in only arrives once the
       page has moved, so at rest the bar is the hero's own sky. */
    .navdock {
      position: fixed !important;
      left: clamp(14px, 3.2vw, 44px) !important;
      right: clamp(14px, 3.2vw, 44px) !important;
    }
    /* Wider than the 1248 content column on purpose -- the dock is an object
       floating over the page, not a lid on the text column. The resting blur
       is 0 rather than `none` so it has somewhere to travel from: a filter
       list does not interpolate out of the keyword. */
    .navbar {
      max-width: 1440px !important;
      background-color: transparent !important;
      border-color: transparent !important;
      box-shadow: none !important;
      backdrop-filter: blur(0px) saturate(100%);
      -webkit-backdrop-filter: blur(0px) saturate(100%);
      transition: background-color 260ms ease, border-color 260ms ease, box-shadow 260ms ease, backdrop-filter 260ms ease, -webkit-backdrop-filter 260ms ease;
    }
    .navrow { height: 50px !important; }
    /* Glass, so the sky and whatever is passing under the pill stay legible
       through it. The fill stops at 0.75 rather than going thinner: the dark
       band scrolls under here too, and the nav text is a mid grey that a
       darkened backdrop would swallow. An open menu takes the same glass: at
       the top of the page the bar is clear and the panel hanging off it is
       not, so the two read as unrelated objects unless the bar comes with it. */
    :root[data-scrolled] .navbar,
    :root[data-nav-open] .navbar {
      background-color: rgba(255, 255, 255, 0.75) !important;
      border-color: rgba(255, 255, 255, 0.7) !important;
      box-shadow: 0 1px 2px rgba(31,29,26,0.04), 0 10px 30px -12px rgba(31,29,26,0.16) !important;
      backdrop-filter: blur(16px) saturate(180%);
      -webkit-backdrop-filter: blur(16px) saturate(180%);
    }

    /* The wordmark text is redundant at rest -- the mark alone sits over the
       hero's own "VoiceIQ Labs" headline -- and only earns its place once
       scrolling has carried the page's own name out of view. Width, not
       display, so the reveal slides rather than snaps. */
    /* Big over the hero, the pill's own size once scrolling gives it a
       reason to shrink -- the same idea as the gender-affirming lander's
       logo, sized for the plain mark instead of bespoke art. */
    .navbrand > img { transform: scale(1); transform-origin: left center; transition: transform 220ms ease; }
    :root:not([data-scrolled]):not([data-nav-open]) .navbrand > img {
      transform: scale(1.25);
    }

    .navbrand { gap: 0; transition: gap var(--nav-wordmark-in) ease; }
    :root[data-scrolled] .navbrand,
    :root[data-nav-open] .navbrand {
      gap: var(--nav-brand-gap);
    }
    .navwordmark {
      display: inline-block; max-width: 0; overflow: hidden;
      white-space: nowrap; opacity: 0;
      transition: max-width var(--nav-wordmark-in) ease, opacity var(--nav-wordmark-fade) ease;
    }
    :root[data-scrolled] .navwordmark,
    :root[data-nav-open] .navwordmark {
      max-width: var(--nav-wordmark-w); opacity: 1;
    }

    .navtoggle {
      display: none; flex-direction: column; align-items: center; justify-content: center;
      gap: 5px; width: 34px; height: 34px; padding: 0;
      background: transparent; border: 0; border-radius: 10px; cursor: pointer;
      -webkit-tap-highlight-color: transparent;
    }
    .navtoggle span {
      display: block; width: 17px; height: 1.5px; border-radius: 2px; background-color: var(--color-ink);
      transition: transform 240ms cubic-bezier(0.4, 0, 0.2, 1);
    }
    /* The two bars cross rather than a third fading out: gap 5 on 1.5 puts each
       centre 3.25 off the middle, so that is the distance each has to travel. */
    :root[data-nav-open] .navtoggle span:first-child { transform: translateY(3.25px) rotate(45deg); }
    :root[data-nav-open] .navtoggle span:last-child { transform: translateY(-3.25px) rotate(-45deg); }

    /* Out of flow, so a closed menu leaves no invisible strip below the bar for
       the page's own taps to land in. It carries its own fill rather than the
       dock's, because it can be open at the top of the page where the bar is
       still glass-clear. */
    .navmenu {
      display: none; position: absolute; top: 100%; left: 0; right: 0; margin-top: 10px;
      padding: 7px; border-radius: 22px;
      border: 1px solid rgba(255, 255, 255, 0.7);
      background-color: rgba(255, 255, 255, 0.9);
      backdrop-filter: blur(16px) saturate(180%);
      -webkit-backdrop-filter: blur(16px) saturate(180%);
      box-shadow: 0 1px 2px rgba(31,29,26,0.04), 0 18px 40px -14px rgba(31,29,26,0.28);
    }
    .navmenu a {
      display: block; padding: 12px 16px; border-radius: 16px;
      font-size: 14.5px; font-weight: 500; color: var(--color-body);
      transition: background-color 160ms ease, color 160ms ease;
    }
    .navmenu a:hover, .navmenu a:focus-visible { background-color: rgba(31,29,26,0.05); color: var(--color-ink); }
    .navmenu a[aria-current] { color: var(--color-ink); background-color: rgba(31,29,26,0.05); }

    @media (prefers-reduced-motion: reduce) {
      .navlink, .navlink::after, .navbar, .navbrand, .navbrand > img, .navwordmark, .navtoggle span, .navmenu, .navmenu a, .tab { transition: none; }
    }

    /* A fixed side column shrinks before anything wraps; the row breaks
       only once the column beside it is down to its own minimum. */
    div:has(> .side) { flex-wrap: wrap; }
    .side { flex-shrink: 1 !important; min-width: 0; }
    div:has(> .side) > *:not(.side) { min-width: min(100%, 330px); }

    /* grids fit as many columns as the row can hold. The floor is capped at
       100% so the final single column can still shrink: a bare minmax(Npx,..)
       stops at N and overflows a narrower row rather than following it. */
    [style*="grid-template-columns: repeat(4"] { grid-template-columns: repeat(auto-fit, minmax(min(100%, 196px), 1fr)) !important; }
    [style*="grid-template-columns: repeat(3"] { grid-template-columns: repeat(auto-fit, minmax(min(100%, 248px), 1fr)) !important; }
    [style*="grid-template-columns: repeat(2"] { grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)) !important; }

    /* 60px is the home headline and nothing else. Its floor is above the
       ladder the rest of the scale would put it on: on a phone that line is
       the whole first screen, and the ladder's 29px reads as body copy. */
    [style*="font-size: 60px"] { font-size: clamp(34px, 3.6vw, 53px) !important; }
    [style*="font-size: 56px"] { font-size: clamp(28px, 3.4vw, 50px) !important; }
    [style*="font-size: 52px"] { font-size: clamp(27px, 3.1vw, 46px) !important; }
    [style*="font-size: 50px"] { font-size: clamp(26px, 3.0vw, 44px) !important; }
    [style*="font-size: 48px"] { font-size: clamp(26px, 2.9vw, 42px) !important; }
    [style*="font-size: 44px"] { font-size: clamp(25px, 2.6vw, 39px) !important; }
    [style*="font-size: 40px"] { font-size: clamp(24px, 2.4vw, 36px) !important; }
    [style*="font-size: 36px"] { font-size: clamp(22px, 2.2vw, 32px) !important; }
    [style*="font-size: 34px"] { font-size: clamp(21px, 2.0vw, 30px) !important; }
    [style*="font-size: 30px"] { font-size: clamp(20px, 1.8vw, 27px) !important; }
    [style*="font-size: 26px"] { font-size: clamp(19px, 1.6vw, 24px) !important; }
    [style*="font-size: 24px"] { font-size: clamp(18px, 1.5vw, 22px) !important; }
    /* The bottom of the ladder is body copy, and a floor is the phone size.
       These sit above where the slope would leave them: the draft's own 16px
       paragraphs carry no rule at all and so land on a phone at 16, which is
       the size the text around them has to read against. 22 moves with them
       only to stay above 20. */
    [style*="font-size: 22px"] { font-size: clamp(17px, 1.4vw, 20px) !important; }
    [style*="font-size: 20px"] { font-size: clamp(16.5px, 1.3vw, 18px) !important; }
    [style*="font-size: 19px"] { font-size: clamp(16px, 1.25vw, 17.5px) !important; }
    [style*="font-size: 18px"] { font-size: clamp(15.5px, 1.2vw, 17px) !important; }

    /* Vertical rhythm was drawn for a 1440 canvas. The hero is left out of
       this because its top padding is not rhythm at all — it is what clears
       the floating nav.

       The floor is what a phone gets. It was 28px, chosen to spend as little
       of a narrow screen on nothing as the layout could stand, and that is
       the wrong thing to be thrifty with here: every section boundary on
       this page is a hard edge between two background colours, so the
       padding is the only thing holding a heading off the band above it. At
       28 the bands ran together and each one read as short. 44 is the floor
       instead — 88px between one section's last line and the next one's
       first. The ramp is unchanged, so nothing above 786px moves. */
    @media (max-width: 1240px) {
      .page > div:not(.navdock):not(.sitefoot):not(:nth-child(2)) {
        padding-top: clamp(44px, 5.6vw, 96px) !important;
        padding-bottom: clamp(44px, 5.6vw, 96px) !important;
      }
      /* The closing CTA is the one section drawn with more room above it
         than the rest — 144px against their 96 — and the rule above flattens
         every section onto one ramp, so the page's last beat arrived with the
         same 28px lead-in as everything else. 1.5x the floor and the slope
         both, which is the ratio it was drawn at. It belongs in this query
         and not the phone one: the ramp it overrides runs to 1240, and a
         steeper ramp that stopped at 720 would hand the section *less*
         headroom at 721 than at 719. Specificity ties, so source order is
         what settles it — this has to stay below the rule it overrides. */
      .page > div.sky:not(.sitefoot):has(+ .sitefoot) {
        padding-top: clamp(66px, 8.4vw, 96px) !important;
      }
    }
    /* What the hero has to clear is the same 68px on every screen — the dock
       is fixed 16px down and 52px tall, and neither figure follows the
       viewport. So below the draft width the padding tracks that instead of
       the sky it was drawn as: 176px of sky is most of a phone's first
       screen, and the headline starts below the fold it should be opening. */
    @media (max-width: 1100px) {
      .hero { padding-top: clamp(112px, 13vw, 143px) !important; }
      /* The hero's 80px sat outside the rhythm rule above, so on a phone it
         stacked onto the next section's floor for 120px between the showcase
         card and the premise -- the widest gap on the page, under the one
         block a reader is most likely to still be looking at. 7.3vw meets
         the authored 80 at 1096, so this only ever takes width away. */
      .hero { padding-bottom: clamp(36px, 7.3vw, 80px) !important; }
    }

    /* A section marked .attached carries on the band above it instead of
       opening a new one, and the two floors above are both sized for the case
       it is not: a hard edge between two background colours. The research
       headline and the chart card it introduces sit on one unbroken wash and
       were drawn 36px apart as a single unit, so on a phone the floors pull
       them to 80 and the card lands most of a screen below the line that names
       it. The pair keeps what it was drawn with instead.

       Both values are restated rather than released, because what they restore
       is inline on the element and there is no way to hand that back -- revert
       reaches past the author origin, not into it. Only the top edge is
       exempt: the section's bottom is a real boundary and keeps the ramp.

       The chart rule has to outrank a four-class selector, hence the two
       :not()s, which are the same two that rule already spends them on. */
    @media (max-width: 1240px) {
      .page > div.sky.attached:not(.navdock):not(.sitefoot) { padding-top: 16px !important; }
    }
    @media (max-width: 1100px) {
      .hero:has(+ .attached) { padding-bottom: 20px !important; }
    }

    @media (max-width: 720px) {
      .navlinks, .navsignin { display: none !important; }
      .navrow { padding: 0 12px 0 18px !important; }
      .navactions { gap: 10px !important; }
      .navtoggle { display: inline-flex; }
      /* visibility rather than display, so the drop has something to animate;
         it trails the transform on the way out so the panel is not gone before
         it has finished leaving. */
      .navmenu {
        display: block; opacity: 0; visibility: hidden; transform: translateY(-8px);
        transition: opacity 200ms ease, transform 220ms cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 220ms;
      }
      :root[data-nav-open] .navmenu {
        opacity: 1; visibility: visible; transform: translateY(0);
        transition: opacity 200ms ease, transform 220ms cubic-bezier(0.4, 0, 0.2, 1), visibility 0s;
      }
      /* Two columns, not one: a measurement's name and the scale it reads on
         are both short, and stacking all three cells spends a line on each
         row for a phrase that fits beside the name. The description is the
         only cell that needs the width, and it takes the second line alone --
         placed explicitly, because source order runs name, description,
         scale. Its desktop padding-right held it off the scale column, which
         on one line is dead space at the end of every wrap. */
      [style*="grid-template-columns: 190px 1fr 210px"] {
        grid-template-columns: minmax(0, 1fr) auto !important;
        align-items: baseline !important;
        column-gap: 12px !important; row-gap: 8px !important;
      }
      [style*="grid-template-columns: 190px 1fr 210px"] > :nth-child(1) { grid-area: 1 / 1; }
      [style*="grid-template-columns: 190px 1fr 210px"] > :nth-child(2) { grid-area: 2 / 1 / auto / -1; padding-right: 0 !important; }
      [style*="grid-template-columns: 190px 1fr 210px"] > :nth-child(3) { grid-area: 1 / 2; }
      /* The definition is now a second line inside the row rather than a
         third column, so it gets a hairline to sit under -- and the row's own
         divider goes a step darker to stay the stronger of the two. Without
         that the inner line and the boundary are the same weight and every
         measurement reads as two rows. The header carries no inner line: its
         three labels are one strip, not a name and its definition. */
      .defrow > :nth-child(2) { border-top: 1px solid var(--color-hairline); padding-top: 8px; }
      .defrow { border-bottom-color: var(--color-hairline) !important; }
      /* Stacked, the 16px this row was drawn with is no longer the space
         between two things sitting side by side — it is the space between
         the logo and a row of links, and between the disclaimer and the
         copyright, with 25 to 56px around every other boundary in the
         footer. At that size each pair reads as one block rather than two.
         28px is the floor the page's own section rhythm falls back to. */
      .sitefoot-row { flex-direction: column !important; align-items: flex-start !important; gap: 28px !important; }
      /* Stacking the row put the five links on their own line but left them
         in a nowrap flex, so the line could only be paid for by squeezing:
         "How it works" collapses to three stacked words at 390px and the
         row still ends 43px past the gutter at 320. It wraps instead. The
         32px gap goes with it — that was one line's worth of separation,
         and across two lines it reads as columns that do not line up. */
      .sitefoot-row > div:last-child { flex-wrap: wrap !important; gap: 10px 20px !important; }
      /* The counter and the headline read as one unit at 1440 — 28px of gap
         under a 60px line. At phone type the line is half that, so the same
         28px reads as a collision; the gap opens rather than scaling with the
         type it sits over. */
      .herostat { margin-bottom: 56px !important; }
      /* The counter line does not fit a phone whole, and "recordings" is the
         one word on it that can leave without stranding a number from its
         unit. It drops under its own stat rather than under the line:
         .statwords dissolves into .statgroup so the number and "analyzed"
         share one flex row -- which is also what swallows the stray text node
         between the two words -- and "recordings" hangs out of flow beneath
         it, over the padding that is now the line it occupies. Out of flow is
         the fix, not the mechanism: as a wrapped flex item it still paid for
         its own width on the row it had left, so the group reserved a
         word-and-a-half of nothing to the right and carried the whole counter
         that far left of centre. Centring it in the line instead would centre
         it under the voices half as well. */
      .statgroup { position: relative; display: inline-flex; align-items: baseline; padding-bottom: 18px; }
      .statwords { display: contents; }
      .statwords > span:first-child { position: absolute; top: 18px; left: 0; right: 0; }
      /* The lede's 40px was drawn under a single line of it. On a phone it
         wraps to two or three, and the leading of the last one is gap the
         draft never had to pay for. */
      .herolede { margin-bottom: 28px !important; }
      .heroshow { margin-top: 40px !important; }
      /* Three columns on one line do not survive a phone: at 390px the name
         and the reading leave the track about 48px, and under 340px the two
         of them are already wider than the row, so the track shrinks to
         nothing. The track takes a line of its own instead — the full width
         of the card, which is the only width at which a 13% band and a marker
         sitting inside it read as a measurement rather than a smudge. The
         descriptor is the row's second span and stays out: the name and the
         reading share their line with no room to spare at 320px. */
      /* Asymmetric on purpose: the space under a bar is where its hover
         label prints, so it is 17px of label plus air, not rhythm. */
      [data-showcase-row]:not(.gaugecard) { flex-wrap: wrap; gap: 14px 12px !important; padding: 12px 0 20px !important; }
      .mlabel { width: auto !important; }
      .mlabel span + span { display: none !important; }
      .mtrack { order: 2; flex: 1 0 100% !important; }
      [data-showcase-value] { width: auto !important; margin-left: auto; }
      /* The hover labels hang under the track here rather than rising off
         it. Above, they land in the gap the reading sits in -- the marker's
         reaches 18px, off a dot that overhangs the track by 3px, and the
         band names itself from wherever the band happens to sit, which on
         the Articulation row is directly under the number. Clearing that
         costs more distance between a reading and its own bar than the two
         should ever have. Below, the row's own bottom padding already holds
         them, and the gap above is free to be whatever looks right. */
      .mtip { top: 100%; bottom: auto; margin: 3px 0 0; }
      /* .mband ends at the track; .mpoint's dot overhangs it by 3px. Below
         the track that difference is the label's own offset, so the band's
         carries the 3px back to sit level with the marker's. */
      .mtip-band { margin-top: 6px; }
      /* The passage title and the readout beside it share one row, and by the
         time the card is this narrow the readout cannot give -- it is a single
         unbroken line. So the title is what yields. Capped in em and not px
         because the type under it is a clamp, and a px cap drawn at 22px is a
         wider box in ems by the time the line renders at 17: 9.6em sits
         between the width of "The North Wind and" and that of the same plus
         "the", so the break lands after "and" at every size this line takes. */
      .herotitle { max-width: 9.6em; }
      /* The count hangs below the header to land just over the first metric
         row, and the padding under the header is what it hangs through -- so
         it costs no height, on a phone or anywhere. What does cost height is
         that padding itself, under a title the rule above has just broken
         onto a second line: the phone spends 12px of it rather than 26, which
         brings the rows up. The hang gives up more again, so the count sits
         off the first row by more here than it does at full width. */
      .herohead { padding-bottom: 0 !important; }
      .herometa { margin-bottom: -4px !important; }
      [data-showcase]:not(.gaugeshow) { padding-top: 12px !important; }
      /* 28px of side padding is a sixth of a phone's card, and now that the
         track spans the card it is a sixth of the track. */
      .herohead, [data-showcase]:not(.gaugeshow) { padding-left: 20px !important; padding-right: 20px !important; }
      /* The gauge card answers to the same [data-showcase-*] hooks the home
         page's metric rows do, because showcase.js animates both -- but a row
         in a list has no padding to keep and a card does, so the rules above
         would strip this one back to its border. It stacks under the copy
         here, on the left edge the heading above and the table below share:
         a card indented past both reads as a stray. */
      /* Step 3 is the only one drawn card-left, so wrapping puts its card
         above the header that introduces it -- three target sliders arrive
         before the word PLAN, and the number 3 shows up under the thing it
         was meant to number. Ordered back under the copy, where the other
         three steps' cards already land, and the 64px the row was drawn with
         comes down to the gap a stacked pair reads as one unit at. */
      div:has(> .targetscard) { gap: 28px !important; }
      .targetscard { order: 2; }
      .gaugeshow { gap: 24px !important; margin-bottom: 32px !important; }
      .gaugecard { padding: 16px 18px 18px !important; }
      /* The reading and its descriptor are one unit -- margin-left: auto on
         the value carries the pair to the right, and they wrap together to a
         second line rather than crushing the name. */
      .gaugecard > div:first-child { flex-wrap: wrap; column-gap: 10px; row-gap: 4px; }
      /* Nothing hangs under this track. The home page's rows spend that space
         on hover labels; here it was 34px of empty card. */
      .gaugecard > div:nth-child(2) { margin: 14px 0 0 !important; }
      /* Every side column is drawn at a fixed width beside something
         wider. Once the row has wrapped, that width is no longer a column --
         it is a block against the left gutter with the rest of the line
         empty beside it, which reads as the section having slipped rather
         than stacked. Wrapped, the column takes the whole line. */
      .side { flex-basis: 100% !important; width: auto !important; }

      /* The premise stacks copy-then-card on a phone, which puts the second
         paragraph -- the one that says what the product does about any of
         this -- below a card the height of most of the screen, past where
         the section already reads as finished. The card goes between the two
         paragraphs instead. display: contents promotes the copy's children
         into the same flex row the card is in, so this is one row reordering
         itself rather than a second copy of a paragraph hidden at the other
         breakpoint. */
      .premise { gap: 0 !important; }
      .premise-copy { display: contents; }
      .premise-copy > * { flex-basis: 100%; }
      .premise > .side { order: 2; flex-basis: 100% !important; margin: 8px 0 20px; }
      .premise-copy > p:last-child { order: 3; }
      /* Four milestones down one column is eight short lines and most of a
         phone screen; two across halves that. The dot moves inside each cell
         -- the single 20px indent the column was drawn with cannot serve two
         columns, since a dot hung to the left of the right-hand one lands in
         the gutter between them and reads as belonging to whichever text it
         happens to sit nearer. The spine goes with the column it was drawn
         for: one line down the left of a 2x2 connects half the dots and
         points past the other half. The week ranges carry the order. */
      .timeline { display: grid !important; grid-template-columns: 1fr 1fr; gap: 18px 16px !important; padding-left: 0 !important; }
      .timeline-step { padding-left: 20px; }
      .timeline-step > .timeline-dot { left: 0 !important; }
      .timeline-spine { display: none; }
      /* 11px was a caption beside a chart on a 1440 canvas. On a phone the
         chart is above it and this is the paragraph closing the section, at
         a size below what a phone should ask anyone to read. It stays under
         the 15px line it follows on weight and colour rather than on size --
         at 14 the two are a point apart, which is close, but shrinking a
         paragraph to protect a hierarchy the eye already reads is the wrong
         trade. */
      .sidenote { font-size: 14px !important; }
      /* The chart is a 520x190 viewBox in a box fixed at 190 tall. Once the
         card is narrower than 520 the drawing scales to the width and the
         height stops being filled — at 390px it renders at 54% and sits in
         a 102px band centred in the box, so 88px of the card is empty above
         and below the line. Letting the height follow the width drops that
         band, and the drawing is the same size either way: the scale was
         already the width's to set. The cap is the other half of it. Between
         520 and this query's own edge the width would carry the box past the
         height it was drawn at and scale the chart up past 1:1, which is a
         bigger chart than the card was built for rather than a tighter one. */
      [data-progress-chart] { height: auto !important; max-height: 190px; }
      .cta:not(.cta-sm) { width: 100%; }
    }

    /* ── the research chart on a phone ───────────────────
       Everything the chart draws lives in a 1100x400 viewBox stretched
       to whatever box the SVG lands in, so its margins are shares of
       that box rather than measurements: the same gutter that holds a
       tick label at 1080px wide holds nothing at 280. The component
       picks those shares itself below this same breakpoint -- keep
       narrow() in Research.dc.html and the query here in step -- and
       what is left here is the type, the padding, and the controls. */
    /* The Measurement and Split-by controls are buttons that open a
       floating panel, not native <select>s -- a native picker's open
       animation belongs to the platform, not to us, and cannot slide.
       Open state lives as a flag on <html> (dropdown.js), not on the
       panel itself, because /research replaces its whole body -- the
       panel included -- on every control change, and a flag on the
       replaced subtree would vanish with it. */
    .dcdd { position: relative; }
    .dcdd-trigger {
      display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
      font-family: inherit; font-size: 13px; color: var(--color-ink); background-color: var(--color-white);
      border: 1px solid var(--color-hairline); border-radius: 9px; padding: 8px 12px;
      cursor: pointer; text-align: left;
      transition: border-color 150ms ease, box-shadow 150ms ease;
    }
    .dcdd-trigger:hover { border-color: var(--color-line); }
    .dcdd-trigger:focus-visible { outline: none; border-color: var(--color-info); box-shadow: 0 0 0 3px rgba(37,95,152,0.15); }
    .dcdd-trigger-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .dcdd-chevron { flex-shrink: 0; color: var(--color-faint); transition: transform 200ms ease; }
    :root[data-dd-open="metric"] [data-dd-trigger="metric"],
    :root[data-dd-open="facet"] [data-dd-trigger="facet"] {
      border-color: var(--color-info); box-shadow: 0 0 0 3px rgba(37,95,152,0.15);
    }
    :root[data-dd-open="metric"] [data-dd-trigger="metric"] .dcdd-chevron,
    :root[data-dd-open="facet"] [data-dd-trigger="facet"] .dcdd-chevron { transform: rotate(180deg); }

    /* Anchored under its own trigger, never the page -- top/left are relative
       to .dcdd, not the viewport, so the panel cannot end up somewhere else. */
    .dcdd-panel {
      position: absolute; top: calc(100% + 8px); left: 0; z-index: 30;
      min-width: 100%; width: max-content; max-width: min(320px, calc(100vw - 48px));
      max-height: 340px; overflow-y: auto;
      background-color: var(--color-white); border: 1px solid var(--color-hairline); border-radius: 12px;
      box-shadow: 0 16px 40px -12px rgba(31,29,26,0.22), 0 2px 8px rgba(31,29,26,0.05);
      padding: 6px;
      opacity: 0; visibility: hidden; pointer-events: none;
      transform: translateY(-6px) scale(0.98); transform-origin: top left;
      transition: opacity 160ms ease, transform 180ms cubic-bezier(0.16,1,0.3,1), visibility 0s linear 180ms;
    }
    :root[data-dd-open="metric"] [data-dd-panel="metric"],
    :root[data-dd-open="facet"] [data-dd-panel="facet"] {
      opacity: 1; visibility: visible; pointer-events: auto;
      transform: translateY(0) scale(1);
      transition: opacity 160ms ease, transform 200ms cubic-bezier(0.16,1,0.3,1), visibility 0s linear 0s;
    }
    .dcdd-group-label {
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
      font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-dim);
      padding: 10px 10px 4px;
    }
    .dcdd-group-label:first-child { padding-top: 6px; }
    .dcdd-option {
      display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%;
      font-family: inherit; font-size: 13px; color: var(--color-ink); background-color: transparent;
      border: 0; border-radius: 8px; padding: 8px 10px; text-align: left; cursor: pointer;
      transition: background-color 120ms ease;
    }
    .dcdd-option:hover, .dcdd-option:focus-visible { background-color: var(--color-well); outline: none; }
    .dcdd-option[data-active="true"] { background-color: rgba(37,95,152,0.08); color: var(--color-info); font-weight: 600; }
    .dcdd-option[data-active="true"]::after { content: ""; width: 6px; height: 6px; border-radius: 9999px; background-color: var(--color-info); flex-shrink: 0; }

    /* ── the box view's axis rail ─────────────────────────
       A gutter written in viewBox units is a share of the plot's width,
       and the box view is the one view whose plot is not the card's
       width: it stretches past it to give every group a readable slot
       and scrolls to it. A 13% gutter of that stretched band is 240px,
       which carries the tick labels into the middle of the chart and
       then scrolls them away with the boxes. So the box view's axis is
       a real column beside the scroller -- fixed px, outside the
       overflow -- and its band runs the full 1100 of the viewBox. Only
       the curve view, which never scrolls, still draws its own gutter. */
    .chartaxis { position: relative; flex-shrink: 0; }
    .chartscroll {
      position: relative; min-width: 0; overscroll-behavior-x: contain;
      scrollbar-width: thin; scrollbar-color: var(--color-line) transparent;
    }
    .chartscroll::-webkit-scrollbar { height: 6px; }
    .chartscroll::-webkit-scrollbar-track { background-color: transparent; }
    .chartscroll::-webkit-scrollbar-thumb { background-color: var(--color-line); border-radius: 9999px; }
    /* Nothing in the markup says whether the band overflows -- that is a
       measurement, so afterPaint sets the flag and clears it at the end
       of the scroll. Without it the last group looks like the last one. */
    .chartscroll[data-more="1"] {
      -webkit-mask-image: linear-gradient(to right, var(--color-black) calc(100% - 36px), transparent);
      mask-image: linear-gradient(to right, var(--color-black) calc(100% - 36px), transparent);
    }

    @media (max-width: 640px) {
      [data-chart-card] > div { padding-left: 14px !important; padding-right: 14px !important; }
      /* Column, not the old flex-basis-100% wrap hack -- that left the view
         toggle pinned to its own content width while the two dropdowns above
         it went full-bleed, so the row of three read as two finished
         controls and one unfinished one. Stretching all three the same way
         and giving each a real touch height reads as one designed toolbar
         instead of three separately-sized inputs stacked by accident. */
      [data-chart-card] > div:first-child {
        flex-direction: column !important;
        align-items: stretch !important;
        gap: 16px !important;
        padding-top: 16px !important;
        padding-bottom: 18px !important;
      }
      .dcdd-trigger { height: 42px; font-size: 14px; }
      /* Full width of the stacked column, not max-content -- a panel sized
         to its longest option (the crossed facets) would overhang the card
         on a phone instead of sitting flush under its own trigger. */
      .dcdd-panel { left: 0; right: 0; width: auto; max-width: none; }
      [data-chart-card] > div:first-child > div:last-child > div { width: 100%; }
      [data-chart-card] > div:first-child > div:last-child button { flex: 1 1 50%; padding-top: 11px !important; padding-bottom: 11px !important; font-size: 13px !important; }
      .chartrow { gap: 6px !important; }
      .chartrow > div:first-child { width: 16px !important; }
      .ytick { font-size: 10px !important; padding-right: 5px !important; }
      .xtick { font-size: 10px !important; }
      .peaklabel { font-size: 10.5px !important; }
      .legendchip { font-size: 11.5px !important; padding: 4px 10px 4px 8px !important; }
      .legendmore { padding: 4px 11px !important; }
      .charttip { font-size: 11px !important; padding: 7px 9px !important; }
    }


  /* One sky pattern runs the height of .page. A section's --strength says how
     much of it shows through (0 = none, 1 = full); the veil ramps to the
     midpoint with each neighbour over --blend, so a change in strength eases
     across the boundary and an unchanged one stays flat. */
  .page > .sky { position: relative; isolation: isolate; }
  .page > .sky::before {
    content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background-image: linear-gradient(180deg,
      rgba(255, 255, 255, calc(1 - var(--from, var(--strength, 0)))) 0%,
      rgba(255, 255, 255, calc(1 - var(--strength, 0))) var(--blend, min(120px, 30%)),
      rgba(255, 255, 255, calc(1 - var(--strength, 0))) calc(100% - var(--blend, min(120px, 30%))),
      rgba(255, 255, 255, calc(1 - var(--to, var(--strength, 0)))) 100%);
  }
  /* Grain rides on top of the veil rather than under it, so its tooth reads
     the same in a section the veil has whitened out as in one showing full
     sky. Both pseudo-elements sit at -1 and paint in tree order. */
  .page > .sky::after {
    content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    opacity: 0.12;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='grain'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.4' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23grain)'/%3E%3C/svg%3E");
  }

  /* Blocks rise into place as the page is read. The hidden state travels with
     data-rise, which only reveal.js sets and only alongside the observer that
     clears it -- these two rules are inert on a page that script does not run.

     The motion belongs to the arrival and not to the hiding. A transition on
     data-rise instead animates the block *out* the moment the script marks it,
     so a block the observer clears in the same breath -- every block above the
     fold -- turns round at opacity 0.99 and never appears to arrive at all. */
  [data-rise] { opacity: 0; transform: translateY(16px); }
  [data-rise][data-risen] {
    opacity: 1; transform: none;
    transition: opacity 600ms cubic-bezier(0.4, 0, 0.2, 1), transform 600ms cubic-bezier(0.4, 0, 0.2, 1);
  }

  /* The chart card's metric switches. progress.js owns aria-pressed, and both
     states are drawn from it here so the page still says which metric is on the
     chart when the script has not run and the buttons do nothing. */
  .tab {
    border: 0; border-radius: 9999px; padding: 7px 14px; cursor: pointer;
    font-family: inherit; font-size: 10px; font-weight: 500; letter-spacing: 0.05em;
    text-transform: uppercase; background-color: var(--color-hairline); color: var(--color-faint);
    transition: background-color 200ms ease, color 200ms ease;
  }
  .tab:hover { color: var(--color-body); }
  .tab[aria-pressed="true"], .tab[aria-pressed="true"]:hover { background-color: var(--color-ink); color: var(--color-page); }

  /* The plan's four milestones walk themselves (timeline.js). A filled dot
     travels between them and tucks in behind each milestone it reaches, which
     is the moment that milestone takes the accent and keeps it -- so the run
     reads as a plan being delivered rather than as a light switching around.

     Behind is the whole effect and it is the z-index pair that buys it, not
     the source order: the marker is a milestone's white middle, filled and in
     transit, so it has to vanish under the real one rather than sit on it.
     The size written here is that middle on the artboard's own dot, and
     timeline.js measures the real one back off it -- a dot redrawn at another
     size would otherwise leave the marker peeking out from under it.

     One element in the timeline's box, rather than a circle on each dot:
     travel is then a single transform between two places, and it has to hold
     on a phone too, where the column becomes a 2x2 grid and the next
     milestone is across as well as down.

     Nothing here draws until timeline.js places the marker, and that script
     stands down under reduced motion, so what both get is the artboard's own
     page: the first milestone lit and nothing moving. */
  .timeline-title { transition: color 500ms ease; }
  .timeline-dot { z-index: 1; transition: border-color 500ms ease; }
  /* The pop is a transform and not a size, so the circle can swell over the
     marker arriving under it without the milestone's text stepping sideways
     -- the 10px dot is what the column's 20px indent is measured to. */
  .timeline-dot[data-pop] { animation: timeline-pop 380ms ease-out; }
  @keyframes timeline-pop { 40% { transform: scale(1.4); } }
  .timeline-marker {
    position: absolute; left: 0; top: 0; z-index: 0; width: 6px; height: 6px;
    border-radius: 9999px; background-color: currentColor; opacity: 0;
    transition: transform 640ms cubic-bezier(0.65, 0, 0.35, 1), opacity 400ms ease;
  }

  /* The how-it-works passage card announces itself as a live capture on
     load: the waveform sweeps in bar by bar and the pitch trace draws
     itself in behind it. The dot keeps pulsing after that, the way a
     recording indicator does, and the
     trace keeps drifting -- a pitch reading never sits still, and a frozen
     line under a blinking record dot reads as a screenshot. The drift is a
     `d` morph rather than a transform so the curve changes shape instead of
     sliding as one rigid piece; every step below carries the same segment
     list (M, C, S, S, S) or the browser cannot interpolate between them. */
  .livecard svg rect {
    transform-box: fill-box; transform-origin: center;
    animation: livecard-bar 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
  }
  .livecard svg rect:nth-child(1) { animation-delay: 0ms; }
  .livecard svg rect:nth-child(2) { animation-delay: 30ms; }
  .livecard svg rect:nth-child(3) { animation-delay: 60ms; }
  .livecard svg rect:nth-child(4) { animation-delay: 90ms; }
  .livecard svg rect:nth-child(5) { animation-delay: 120ms; }
  .livecard svg rect:nth-child(6) { animation-delay: 150ms; }
  .livecard svg rect:nth-child(7) { animation-delay: 180ms; }
  .livecard svg rect:nth-child(8) { animation-delay: 210ms; }
  .livecard svg rect:nth-child(9) { animation-delay: 240ms; }
  .livecard svg rect:nth-child(10) { animation-delay: 270ms; }
  .livecard svg rect:nth-child(11) { animation-delay: 300ms; }
  .livecard svg rect:nth-child(12) { animation-delay: 330ms; }
  .livecard svg rect:nth-child(13) { animation-delay: 360ms; }
  .livecard svg rect:nth-child(14) { animation-delay: 390ms; }
  .livecard svg rect:nth-child(15) { animation-delay: 420ms; }
  .livecard svg rect:nth-child(16) { animation-delay: 450ms; }
  .livecard svg rect:nth-child(17) { animation-delay: 480ms; }
  .livecard svg rect:nth-child(18) { animation-delay: 510ms; }
  .livecard svg rect:nth-child(19) { animation-delay: 540ms; }
  .livecard svg rect:nth-child(20) { animation-delay: 570ms; }
  .livecard svg rect:nth-child(21) { animation-delay: 600ms; }
  .livecard svg rect:nth-child(22) { animation-delay: 630ms; }
  .livecard svg rect:nth-child(23) { animation-delay: 660ms; }
  .livecard svg rect:nth-child(24) { animation-delay: 690ms; }
  @keyframes livecard-bar { from { transform: scaleY(0); } }

  .livecard-path {
    stroke-dasharray: 220; stroke-dashoffset: 220;
    animation: livecard-draw 1500ms cubic-bezier(0.4, 0, 0.2, 1) 240ms both,
               livecard-drift 9s ease-in-out 1740ms infinite;
  }
  @keyframes livecard-draw { to { stroke-dashoffset: 0; } }
  @keyframes livecard-drift {
    0%, 100% { d: path("M2 44 C 22 40, 34 34, 50 33 S 82 39, 98 30 S 128 24, 146 29 S 172 36, 188 31"); }
    25% { d: path("M2 45.6 C 22 38.4, 34 36.2, 50 30.4 S 82 41.2, 98 32.6 S 128 22.2, 146 26.6 S 172 38.4, 188 33.2"); }
    50% { d: path("M2 42.6 C 22 41.8, 34 32.4, 50 35.2 S 82 37.4, 98 27.8 S 128 26.4, 146 31.4 S 172 34.2, 188 28.8"); }
    75% { d: path("M2 44.8 C 22 39.2, 34 35.4, 50 32.2 S 82 40.4, 98 31.6 S 128 23.4, 146 28.2 S 172 37.4, 188 32.4"); }
  }

  /* The Hz readout is livecard.js's: it counts up from the card's arrival
     rather than animating here, so the number the page is served with is
     the one it rests on. */

  .livecard-dot { animation: livecard-pulse 2.6s ease-in-out 2.1s infinite; }
  @keyframes livecard-pulse {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(226, 75, 74, 0.35); }
    50% { opacity: 0.55; box-shadow: 0 0 0 4px rgba(226, 75, 74, 0); }
  }

  @media (prefers-reduced-motion: reduce) {
    .livecard svg rect, .livecard-path, .livecard-dot { animation: none; }
    .livecard-path { stroke-dashoffset: 0; }
  }

  /* The three data cards further down the page settle the same way once
     scrolled into view: reveal.js's data-risen is the trigger for both the
     card's own fade-and-rise and these finer animations inside it, so a
     card that arrives already-visible on load and one a reader scrolls to
     both get the same moment of arrival. Generic because three different
     cards share each of these motions. */
  @keyframes settle-fade { to { opacity: 1; } }
  @keyframes settle-fade-band { to { opacity: 0.10; } }
  @keyframes settle-fade-refline { to { opacity: 0.5; } }
  @keyframes settle-pop { to { opacity: 1; transform: scale(1); } }
  @keyframes settle-grow-x { to { transform: scaleX(1); } }
  @keyframes settle-draw { to { stroke-dashoffset: 0; } }

  /* The gauge card's dot, reading and interquartile band are showcase.js's,
     the same three parts it runs on the home page's metric rows -- the card
     is one of those rows in a different layout, down to the 16px dot and the
     band's tint, so it carries that script's contract rather than a second
     set of keyframes that would drift from it. What is left here is the
     badge, which has no counterpart there. */
  .gaugecard .gauge-badge { opacity: 0; transform: scale(0.8); }
  .gaugecard[data-risen] .gauge-badge {
    animation: settle-pop 320ms cubic-bezier(0.16, 1, 0.3, 1) 620ms both;
  }

  .targetscard .target-fill { transform-origin: left; transform: scaleX(0); }
  .targetscard .target-dot { opacity: 0; transform: scale(0.6); }
  .targetscard[data-risen] .target-row-1 .target-fill { animation: settle-grow-x 460ms cubic-bezier(0.4, 0, 0.2, 1) 0ms both; }
  .targetscard[data-risen] .target-row-2 .target-fill { animation: settle-grow-x 460ms cubic-bezier(0.4, 0, 0.2, 1) 100ms both; }
  .targetscard[data-risen] .target-row-3 .target-fill { animation: settle-grow-x 460ms cubic-bezier(0.4, 0, 0.2, 1) 200ms both; }
  .targetscard[data-risen] .target-row-1 .target-dot { animation: settle-pop 260ms cubic-bezier(0.16, 1, 0.3, 1) 380ms both; }
  .targetscard[data-risen] .target-row-2 .target-dot { animation: settle-pop 260ms cubic-bezier(0.16, 1, 0.3, 1) 480ms both; }
  .targetscard[data-risen] .target-row-3 .target-dot { animation: settle-pop 260ms cubic-bezier(0.16, 1, 0.3, 1) 580ms both; }

  .chartcard .chart-fade, .chartcard .chart-band, .chartcard .chart-refline { opacity: 0; }
  .chartcard .chart-line { stroke-dasharray: 360; stroke-dashoffset: 360; }
  .chartcard .chart-dot, .chartcard .chart-dot-final {
    transform-box: fill-box; transform-origin: center;
    opacity: 0; transform: scale(0.4);
  }
  /* The band's own opacity="0.10" and the ref lines' opacity="0.5" are how
     light they read once in; animating straight to CSS opacity:1 stomps
     those attributes; each element's fade lands where its markup says. */
  .chartcard[data-risen] .chart-fade { animation: settle-fade 360ms ease-out both; }
  .chartcard[data-risen] .chart-band { animation: settle-fade-band 360ms ease-out both; }
  .chartcard[data-risen] .chart-refline { animation: settle-fade-refline 360ms ease-out both; }
  .chartcard[data-risen] .chart-line { animation: settle-draw 680ms cubic-bezier(0.4, 0, 0.2, 1) 80ms both; }
  .chartcard[data-risen] .chart-dot-1 { animation: settle-pop 240ms cubic-bezier(0.16, 1, 0.3, 1) 150ms both; }
  .chartcard[data-risen] .chart-dot-2 { animation: settle-pop 240ms cubic-bezier(0.16, 1, 0.3, 1) 230ms both; }
  .chartcard[data-risen] .chart-dot-3 { animation: settle-pop 240ms cubic-bezier(0.16, 1, 0.3, 1) 310ms both; }
  .chartcard[data-risen] .chart-dot-4 { animation: settle-pop 240ms cubic-bezier(0.16, 1, 0.3, 1) 390ms both; }
  .chartcard[data-risen] .chart-dot-5 { animation: settle-pop 240ms cubic-bezier(0.16, 1, 0.3, 1) 470ms both; }
  .chartcard[data-risen] .chart-dot-6 { animation: settle-pop 240ms cubic-bezier(0.16, 1, 0.3, 1) 550ms both; }
  .chartcard[data-risen] .chart-dot-7 { animation: settle-pop 240ms cubic-bezier(0.16, 1, 0.3, 1) 630ms both; }
  .chartcard[data-risen] .chart-dot-final {
    animation: settle-pop 400ms cubic-bezier(0.16, 1, 0.3, 1) 720ms both;
  }

  @media (prefers-reduced-motion: reduce) {
    .gaugecard .gauge-badge,
    .targetscard .target-fill, .targetscard .target-dot,
    .chartcard .chart-fade, .chartcard .chart-line, .chartcard .chart-dot, .chartcard .chart-dot-final {
      animation: none; opacity: 1; transform: none;
    }
    .chartcard .chart-band { animation: none; opacity: 0.10; }
    .chartcard .chart-refline { animation: none; opacity: 0.5; }
    .chartcard .chart-line { stroke-dashoffset: 0; }
  }

  /* The tail of the measurement table. A <details> rather than a script,
     reversed so the summary sits under the rows it opens -- which is where
     "show fewer" has to be once they are open, and the only place the
     control keeps still between the two states. */
  .morerows { display: flex; flex-direction: column-reverse; }
  /* The rows are hidden here rather than left to the UA's own handling of a
     closed <details>: WebKit has shipped versions where giving the element a
     flex display -- which is what puts the summary under the rows -- leaves
     the contents on screen with the disclosure shut. */
  .morerows:not([open]) .morerows-list { display: none; }
  .morerows > summary {
    list-style: none;
    display: flex; align-items: center; justify-content: center; gap: 8px;
    padding: 14px 26px; cursor: pointer;
    font-size: 13px; font-weight: 600; color: var(--color-body);
    background-color: var(--color-white);
    transition: background-color 160ms ease, color 160ms ease;
  }
  .morerows > summary::-webkit-details-marker { display: none; }
  .morerows > summary:hover { background-color: var(--color-hairline); color: var(--color-ink); }
  .morerows:not([open]) .morerows-open, .morerows[open] .morerows-shut { display: none; }
  .morerows-chevron { transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1); }
  .morerows[open] .morerows-chevron { transform: rotate(180deg); }
  /* Only what is inside the disclosure fades; the height it takes cannot be
     animated without a script, and a fade over a jump reads worse than the
     jump alone if it starts from nothing. */
  .morerows[open] .morerows-list { animation: morerows-reveal 240ms ease-out; }
  @keyframes morerows-reveal { from { opacity: 0; transform: translateY(-4px); } }

  @media (prefers-reduced-motion: reduce) {
    .morerows-list, .morerows-chevron { animation: none; transition: none; }
  }

  /* The footer is a band of its own rather than the tail of the sky section
     above it, so it carries its own palette -- the same ground as the dark
     note on the home page -- and fills the page's content column. */
  .sitefoot { background-color: var(--color-ink); padding: 56px 96px 52px; }
  .sitefoot-inner { max-width: 1248px; margin: 0 auto; }
  .sitefoot-row { display: flex; align-items: center; justify-content: space-between; gap: 24px; }
  .sitefoot-fine { margin-top: 32px; padding-top: 24px; border-top: 1px solid rgba(245,245,243,0.12); }
  /* The disclaimer holds the measure it was drawn at; the row is now as
     wide as the page, which would put it all on one line. */
  .sitefoot-fine > p { max-width: 560px; }
  .sitefoot .navlink { color: var(--color-on-dark-muted); }
  .sitefoot a:hover, .sitefoot .navlink:hover { color: var(--color-surface); }
