/* ==========================================================================
   Hatif landing page
   --------------------------------------------------------------------------
   Sister to the PropIntel and E-Tender landing pages and built the same way:
   duroob tokens, then duroob components, then this file for what is specific
   to the page.

   Like E-Tender and unlike PropIntel, Hatif wears duroob's stock accent rather
   than a venture colour of its own, so it declares no accent layer: every
   accent here resolves through --accent-primary and friends straight out of
   duroob-tokens.css, which is purple on the light page and periwinkle on the
   drenched bands. Nothing in this file re-states an accent value.

   What this page has that its sisters do not is a VOICE. Sound is the product,
   so the one place the page spends its boldness is the utterance — a waveform
   carrying a single sentence — and everything around it is held quiet.
   ========================================================================== */

.page {
  /* The core mark on the stack: the same phrase model the hero panel
     runs, cut as a mask so one shape inherits whatever colour it sits on. */
  --wave-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20103%2044%22%20fill%3D%22%23000%22%3E%3Crect%20x%3D%221.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%224.5%22%20y%3D%2216.1%22%20width%3D%221.6%22%20height%3D%2211.8%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%227.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2210.5%22%20y%3D%2218.4%22%20width%3D%221.6%22%20height%3D%227.2%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2213.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2216.5%22%20y%3D%2216.6%22%20width%3D%221.6%22%20height%3D%2210.8%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2219.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2222.5%22%20y%3D%228.0%22%20width%3D%221.6%22%20height%3D%2228.0%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2225.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2228.5%22%20y%3D%222.0%22%20width%3D%221.6%22%20height%3D%2240.0%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2231.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2234.5%22%20y%3D%228.4%22%20width%3D%221.6%22%20height%3D%2227.2%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2237.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2240.5%22%20y%3D%2212.2%22%20width%3D%221.6%22%20height%3D%2219.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2243.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2246.5%22%20y%3D%2220.8%22%20width%3D%221.6%22%20height%3D%222.5%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2249.5%22%20y%3D%2220.8%22%20width%3D%221.6%22%20height%3D%222.5%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2252.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2255.5%22%20y%3D%2216.1%22%20width%3D%221.6%22%20height%3D%2211.8%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2258.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2261.5%22%20y%3D%2218.4%22%20width%3D%221.6%22%20height%3D%227.2%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2264.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2267.5%22%20y%3D%2216.6%22%20width%3D%221.6%22%20height%3D%2210.8%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2270.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2273.5%22%20y%3D%228.7%22%20width%3D%221.6%22%20height%3D%2226.5%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2276.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2279.5%22%20y%3D%222.3%22%20width%3D%221.6%22%20height%3D%2239.3%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2282.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2285.5%22%20y%3D%229.5%22%20width%3D%221.6%22%20height%3D%2224.9%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2288.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2291.5%22%20y%3D%2211.8%22%20width%3D%221.6%22%20height%3D%2220.4%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2294.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%2297.5%22%20y%3D%2218.2%22%20width%3D%221.6%22%20height%3D%227.7%22%20rx%3D%220.8%22%2F%3E%3Crect%20x%3D%22100.5%22%20y%3D%2220.2%22%20width%3D%221.6%22%20height%3D%223.6%22%20rx%3D%220.8%22%2F%3E%3C%2Fsvg%3E");
  /* A periwinkle wash, not a neutral near white. #F4F4F7 was grey with a
     whisper of blue in it; this leans the ground into the system's own
     periwinkle so the page has a colour rather than an absence of one, and the
     accent sits in a family with its background instead of on top of it.
     periwinkle-50 is #F3F2FA and blue leading, so 70 percent of it into white
     keeps the tint and stays light enough for the ink ratios to hold.

     Everything built on the old ground is rebased on this one. Left pointing
     at #E9E9EE and #F4F4F7 the surfaces and rules would have stayed grey
     against a blue page, which is the way a palette goes muddy. */
  --bg:             color-mix(in srgb, var(--periwinkle-50) 70%, #FFF);
  --surface:        color-mix(in srgb, var(--periwinkle-100) 46%, var(--bg));
  --border:         color-mix(in srgb, var(--periwinkle-300) 46%, var(--bg));
  --border-control: color-mix(in srgb, var(--periwinkle-600) 70%, var(--bg));
  --divider:        color-mix(in srgb, var(--periwinkle-300) 30%, var(--bg));

  /* The dark band base under the photographic bands. NOT a palette step: this
     is the fluted glass texture's OWN signature tone, the one PropIntel's
     stylesheet names when it explains where its indigo ramp was sampled from.
     Darkening a material with its own tone is the one tint that cannot fight
     it, which is the whole point here.

     It replaced --purple-950. That is #1D062F, whose green channel sits 14
     levels UNDER its red, so a band dimmed with it came out measurably violet:
     the copy column averaged R44 G31 B62 and the photograph's blue was buried
     under a purple wash. The same band tinted with the glass tone measures
     R45 G46 B61 — green level with red, blue still 17 clear of it, so the
     colour on screen is the photograph's rather than the paint's.

     This is not PropIntel's indigo. That ramp was drifted toward duroob
     purple and belongs to that product; borrowing it would blur the two. This
     is the raw material both pages are printed on. */
  --ht-ground:      #1D1E30;

  /* Softened panel corner: enough curve to stop reading as a wireframe, short
     of a rounded UI. Controls take it too, so nothing on the page is a pill. */
  --ht-cut: 8px;

  /* A flat scrim over the finished band, the way an overlay sits over a modal.
     The photograph and its gradient are untouched underneath; this is the dial.
     The sisters run 0.40 over a purple scrim; this runs 0.42 over a neutral one
     (periwinkle-1000, the only near-achromatic dark in the set), because the
     two extra points buy back the contrast the warmer tint used to provide and
     the neutral leaves the glass its own colour. */
  --glass-modal: 0.42;

  /* One rhythm for the gap between a section heading and the block it
     introduces, and one optical nudge for a standfirst set beside a title. */
  --ht-head-gap: var(--space-8);
  --ht-optical: .35rem;

  /* Grain over the wash. The same plate the drenched bands use, so the whole
     page is one material rather than two: photographic where it is dark,
     the same tooth where it is light. It sits in the background layer rather
     than as a fixed overlay, so any section with its own ground paints over
     it and the dark bands are not grained twice. soft-light keeps it as
     texture; multiply would dirty the wash. */
  background-color: var(--bg);
  background-image: url("../assets/duroob-texture-grain.jpg");
  background-size: 620px auto;
  background-blend-mode: soft-light;
  color: var(--ink);
  font: var(--text-body);
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* Jumped-to sections start clear of the top edge instead of flush against it. */
#walkthrough, #problem, #actions, #why, #contact { scroll-margin-top: var(--space-8); }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body { margin: 0; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
a { color: inherit; text-decoration: none; }
::selection { background: color-mix(in srgb, var(--accent-primary) 22%, transparent); }
/* duroob.css rings the controls it owns; plain links are left with the browser
   default, which belongs to no design system. */
a:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 3px;
  border-radius: 2px; }
img, svg { max-width: 100%; }

/* Arabic takes the Arabic face, everywhere, automatically. The token set has
   carried Alexandria and a full --text-*-ar scale all along and nothing was
   reaching for it, so Arabic set on this page fell through Geist to whatever
   the system had. A page that claims to speak Arabic naturally cannot render
   Arabic in a fallback: it is the first thing a native reader sees.

   Scoped under .page for specificity, not for reach: every component here sets
   its type with the `font:` SHORTHAND, which carries a family, so a bare
   :lang(ar) ties on specificity and loses to whichever component rule happens
   to come later in the file. .page buys one class and settles it for good. */
.page :lang(ar) { font-family: var(--font-arabic); }

/* ---------- Measure ----------
   .shell owns the page gutter so a background can run edge to edge while its
   content stays on the measure. */
.shell { max-width: 1200px; margin: 0 auto; padding-inline: var(--space-6); }
@media (min-width: 900px) { .shell { padding-inline: var(--space-8); } }

/* Sections do not all weigh the same, so they do not all get the same
   interval. Every non hero section ran padding-block 64px and every head ran
   margin-bottom 32px: six bands of identical height and one repeated gap, which
   is what made the page read flat however the contents were arranged. Three
   weights now, and more space above a heading than below it, so a section
   opens rather than simply starting. */
section { --band: var(--space-8); padding-block: var(--band); }
@media (min-width: 900px) {
  section { --band: var(--space-16); padding-block: calc(var(--band) * 1.35) var(--band); }
  /* The two that carry the product get air. The supporting runs stay tighter,
     which is what makes the air read as a choice. */
  .watch, .edge { --band: calc(var(--space-16) * 1.4); }
  .lost, .doing { --band: var(--space-16); }
}

/* Sections divide on a full-bleed hairline, so the break reads across the whole
   page. The drenched bands carry their own edge and are left out. */
main > section:not(.drenched) + section:not(.drenched) { border-top: 1px solid var(--divider); }

/* ---------- Section heads ---------- */
.sechead { margin: 0 0 var(--ht-head-gap); }
.sechead .sec { margin: 0 0 var(--space-4); }
.sechead .lede { margin: 0; color: var(--ink-secondary); font: var(--text-body-large); }
@media (min-width: 900px) {
  .sechead__in { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    gap: var(--space-8) var(--space-16); align-items: start; }
  .sechead .sec { margin: 0; max-width: 15ch; }
  .sechead .lede { max-width: 54ch; padding-top: var(--ht-optical); }
  /* With no standfirst beside it a title has no reason to be squeezed into the
     left column of a two-column head. */
  /* With the standfirsts gone every head is a bare title. At 26ch they all
     rendered 721px wide and set six identical horizontal bands down the page.
     A headline measure breaks them onto two lines and gives the squint test
     something to tell apart. */
  .sechead__in:not(:has(.lede)) { display: block; }
  .sechead__in:not(:has(.lede)) .sec { max-width: 17ch; }
}

h2.sec { font: var(--text-h1); letter-spacing: -0.025em; margin: 0 0 var(--space-6);
  max-width: 22ch; text-wrap: balance; }
@media (max-width: 700px) { h2.sec { font: var(--text-h2); letter-spacing: -0.015em; } }
p.lede { font: var(--text-body-large); color: var(--ink-secondary); margin: 0; max-width: 68ch; }

/* ---------- The ground, shared by the hero and the close ----------
   Three layers, bottom up: the fluted glass photograph on the element itself, a
   grain plate over it in overlay, then one dim so type sits clear of both. The
   texture is duroob's own — the file the brand book names for dark hero
   surfaces, and the same one both sister pages use. */
.drenched { position: relative; isolation: isolate; overflow: hidden;
  color: var(--ink);
  background-image: url("../assets/duroob-texture-fluted-glass.jpg");
  background-size: cover; background-position: center;
  /* The grain lifts the ground, so the band lifts its secondary text to match.
     The dark theme's own --ink-secondary is periwinkle-400, which measures
     around 3:1 against the brightest rib of this photograph. periwinkle-200
     clears 4.5:1 and keeps the tint. Scoped here, not in the design system. */
  --ink-secondary: var(--periwinkle-200); }

.drenched::before { content: ""; position: absolute; inset: 0; z-index: -2;
  background-image: url("../assets/duroob-texture-grain.jpg");
  background-size: cover; background-position: center;
  mix-blend-mode: overlay; opacity: .62; pointer-events: none; }

.drenched::after { content: ""; position: absolute; inset: 0; z-index: -1;
  background:
    linear-gradient(0deg,
      color-mix(in srgb, var(--periwinkle-1000) calc(var(--glass-modal) * 100%), transparent),
      color-mix(in srgb, var(--periwinkle-1000) calc(var(--glass-modal) * 100%), transparent)),
    linear-gradient(175deg,
      color-mix(in srgb, var(--ht-ground) 45%, transparent) 0%,
      color-mix(in srgb, var(--ht-ground) 56%, transparent) 55%,
      color-mix(in srgb, var(--ht-ground) 68%, transparent) 100%); }

/* A flat dark band. The page already spends the fluted glass photograph on the
   hero and the close, and a third use would make it wallpaper rather than an
   event. This is the plain version: the same ground colour, no texture, so the
   run of sections alternates light and dark without the band competing with
   the two that are meant to. */
.band--ink { background: var(--purple-1000); color: var(--ink);
  --bg: var(--purple-1000);
  --border: color-mix(in srgb, var(--periwinkle-300) 20%, transparent);
  --divider: color-mix(in srgb, var(--periwinkle-300) 14%, transparent);
  --ink-secondary: var(--periwinkle-200); }
main > section.band--ink + section, main > section.band--ink { border-top: 0; }

/* ---------- Navigation, inside the hero band ----------
   It lies over the hero rather than on it: no fill of its own, no shadow, and
   it does not stick on scroll. */
.nav { padding-block: var(--space-4); border-bottom: 1px solid color-mix(in srgb, var(--neutral-50) 14%, transparent); }
.nav__in { display: flex; align-items: center; gap: var(--space-8); }
.brand { display: flex; align-items: baseline; gap: var(--space-1); }
.brand__name { font: var(--text-h3); letter-spacing: -0.02em; color: var(--ink); }
.brand__by { font: var(--text-caption); color: var(--ink-secondary); white-space: nowrap; }
.nav__links { display: none; gap: var(--space-6); margin-inline-start: auto; }
.nav__links a { font: var(--text-body); color: var(--ink-secondary); }
.nav__links a:hover { color: var(--ink); }
@media (min-width: 900px) { .nav__links { display: flex; } }
.nav__cta { display: none; margin-inline-start: auto; }
@media (min-width: 620px) { .nav__cta { display: inline-flex; } }
@media (min-width: 900px) { .nav__cta { margin-inline-start: 0; } }

/* ---------- Hero ----------
   Copy leads and the live call stands beside it. The export carried a third
   element under the copy — three percentages, 86 / 99 / 38, with no source
   against them. They are gone: this page's sister products stake their whole
   position on every figure carrying its provenance, and an unsourced statistic
   on a duroob page undercuts all three. The call panel does the persuading. */
.hero { padding-block: 0 var(--space-16); }
.hero__in { display: grid; gap: var(--space-16); padding-top: var(--space-16); }
@media (min-width: 1000px) {
  /* 624 + 32 + 480 = 1136, the same measure the nav spans, so the panel's right
     edge lands on the gutter instead of hanging past it. */
  .hero__in { grid-template-columns: minmax(0, 624fr) minmax(0, 480fr);
    gap: var(--space-16) var(--space-8); align-items: center; }
}
/* Balanced, unlike E-Tender's. That headline is two short sentences, where
   balance equalises the halves instead of filling each line; this one is a
   single noun phrase, so balance is exactly right — left to wrap naturally it
   broke as "Voice agents for urban / development / operations." and stranded
   one word on the middle line. */
.hero h1 { font: var(--text-display); letter-spacing: -0.03em; margin: 0 0 var(--space-6);
  max-width: 18ch; text-wrap: balance; }
@media (max-width: 900px) { .hero h1 { font: var(--text-h1); letter-spacing: -0.025em; } }
/* A shorter measure than the headline above it: matching the two gives a flat
   left-aligned slab, stepping the second block in gives the rag something to do. */
.hero__lede { font: var(--text-body-large); color: var(--ink-secondary);
  margin: 0 0 var(--space-8); max-width: 470px; }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }

/* ---------- The window chrome ----------
   The frame the walkthrough port sits in, and the same frame PropIntel puts
   its terminal in: a dark application window, light content inside it. */
.viewport { position: relative; border-radius: var(--ht-cut); overflow: hidden;
  background: var(--purple-950);
  border: 1px solid color-mix(in srgb, var(--periwinkle-300) 22%, transparent);
  box-shadow: 0 22px 52px rgba(13, 11, 25, 0.38);
  padding: var(--space-2); padding-top: 0; }
.viewport__bar { display: flex; align-items: center; gap: var(--space-3);
  height: 30px; padding: 0 var(--space-2); }
.viewport__dots { display: flex; gap: var(--space-1); }
.viewport__dots i { width: 6px; height: 6px; border-radius: 1px;
  background: color-mix(in srgb, var(--periwinkle-300) 46%, transparent); }
.viewport__addr { font: var(--text-caption); font-family: var(--font-mono);
  letter-spacing: 0; color: color-mix(in srgb, var(--periwinkle-200) 72%, transparent); }

/* ---------- The call card ----------
   Ported from the design handoff (design_handoff_call_card/call-card.html),
   which is marked high fidelity: colours, type, spacing and motion are its
   numbers, not ours, and they are written out literally below rather than
   mapped onto the page's tokens, so a diff against the handoff is readable.

   Two deliberate departures. The Arabic face is var(--font-arabic), which is
   Alexandria and self hosted, rather than the handoff's IBM Plex Sans Arabic:
   this page ships three woff2 files and no Google Fonts link, and adding a
   network font to a hero costs more than the difference in face. And the
   labels are set in sentence case and uppercased in CSS, which is how every
   other label on this page is written.

   The window it sits in is the page's, not the handoff's: the same dark
   application frame the walkthrough port uses and PropIntel puts its terminal
   in, with the bezel outside that. */
/* No bezel and no window edge. The card sits in the house window, the window
   sat in a gradient frame, and the card had a border of its own: three
   boundaries inside eight pixels. What is left is the window's own ground and
   its shadow, which is all a dark panel on a dark hero needs to lift.

   Scoped to the hero. The walkthrough port keeps its edge, where the window
   sits on a light band and has to hold itself off it. */
.deck .viewport { border: 0; }

/* These tokens are the CARD'S OWN and deliberately outside the duroob set.
   They will be added to the Figma file as a component specific scale rather
   than folded into the system's, so nothing below should be mapped onto
   --periwinkle / --purple steps to make it look tidier. Its type scale is its
   own for the same reason. */
.cc { --cc-bg: #0c0b0f; --cc-card: #111114;
  --cc-s1: #141417; --cc-s2: #18181c; --cc-s3: #19191d; --cc-s4: #1c1c21;
  --cc-l1: #1f1f24; --cc-l2: #232328; --cc-l3: #26262b; --cc-l4: #2a2a30;
  --cc-l5: #2e2e35;
  --cc-t1: #f2f2f4; --cc-t2: #f0f0f2; --cc-t3: #d6d6da; --cc-t4: #b8b8be;
  --cc-t5: #8a8a92; --cc-t6: #6d6d75; --cc-t7: #5f5f66; --cc-t8: #45454c;
  --cc-live: #7fcf8a; --cc-ok: #8fd19a;
  --cc-lilac: #ddd6f0; --cc-lilac-hi: #f1edfb; --cc-cta: #ececee;
  background: var(--cc-card); border-radius: calc(var(--ht-cut) - 2px);
  padding: 20px; color: var(--cc-t2);
  display: flex; flex-direction: column; }
/* Inside the house window the card gives up its own frame: the chrome around
   it is already a frame, and two nested borders two pixels apart is the tell
   of a component pasted into a page rather than built for it. */
.viewport > .cc { border: 0; }
.cc button { font: inherit; padding: 0; cursor: pointer; }
.u-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; }

/* ---- Head: the controls that run it, and what it says about itself ---- */
.cc__head { display: flex; align-items: center; gap: 14px; }
.cc__btn { flex: none; display: flex; align-items: center;
  justify-content: center; border: 1px solid var(--cc-l4);
  transition: background 160ms ease, color 160ms ease; }
.cc__btn svg { display: block; }
.cc__mic { width: 38px; height: 38px; border-radius: 9px;
  background: var(--cc-s3); color: #d8d8dc; }
.cc__mic svg { width: 16px; height: 16px; }
.cc__mute { width: 32px; height: 32px; border-radius: 8px;
  background: transparent; color: var(--cc-t4); }
.cc__mute svg { width: 14px; height: 14px; }
@media (hover: hover) and (pointer: fine) {
  .cc__mic:hover { background: #212126; }
  .cc__mute:hover { color: #FFF; }
}
.cc__mic:active, .cc__mute:active { transform: scale(.96); }
.cc__status { flex: 1; min-width: 0; margin: 0; display: flex;
  align-items: baseline; gap: 10px; font-family: var(--font-mono);
  white-space: nowrap; }
.cc__phase { font-size: 15px; font-weight: 500; letter-spacing: .14em;
  text-transform: uppercase; color: var(--cc-t1); }
.cc__clock { font-size: 15px; color: var(--cc-t5);
  font-variant-numeric: tabular-nums; }
.cc__live { margin: 0; display: flex; align-items: center; gap: 8px;
  height: 28px; padding: 0 12px; border-radius: 999px;
  border: 1px solid var(--cc-l4); font-family: var(--font-mono);
  font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--cc-t3); }
.cc__pip { width: 6px; height: 6px; border-radius: 3px;
  background: #55555c; }
.cc__pip.is-on { background: var(--cc-live); }

/* ---- The waveform ---- */
.cc__wave { position: relative; height: 72px; margin: 22px 0 14px; }
.cc__wave canvas { position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; }
/* ---- What language it is hearing, and who is speaking ---- */
.cc__meta { margin: 0; display: flex; align-items: center; gap: 10px;
  font-family: var(--font-mono); font-size: 12px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--cc-t5); }
.cc__lang { font-family: var(--font-arabic); font-size: 14px;
  letter-spacing: 0; text-transform: none; color: var(--cc-t3); }
.cc__sep { color: var(--cc-t8); }
.cc__spacer { flex: 1; }
.cc__speaker { color: var(--cc-t6); }
.cc__rule { height: 1px; background: var(--cc-l1); margin: 18px 0; }

/* ---- The transcript ---- */
.cc__chat { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: 14px; }
.cc__row { display: flex; align-items: flex-start; gap: 12px;
  opacity: 0; transform: translateY(8px);
  transition: opacity .45s ease, transform .45s cubic-bezier(.2, .8, .2, 1); }
.cc__row.is-on { opacity: 1; transform: none; }
.cc__row--agent { flex-direction: row-reverse; }
.cc__avatar { width: 32px; height: 32px; flex: none; border-radius: 50%;
  background: var(--cc-lilac); border: 1px solid var(--cc-lilac);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: 11px; font-weight: 500;
  color: var(--cc-bg); transition: border-color .3s; }
.cc__avatar.is-speaking { border-color: var(--cc-lilac-hi); }
.cc__bubble { flex: 0 1 auto; min-width: 0; max-width: 66.67%;
  border: 1px solid var(--cc-l3); border-radius: 12px;
  padding: 12px 16px 10px; display: flex; flex-direction: column; gap: 6px;
  background: var(--cc-s2); }
/* The agent's bubble is allowed past the chat's 66.67% because of what is in
   it: lookup:project#NR-114 is an identifier and must not break, and at the
   width this column gives the card it does not fit in two thirds of it. The
   handoff's 560px card never had to answer this. */
.cc__row--agent .cc__bubble { background: var(--cc-s1); gap: 8px;
  max-width: 88%; }
/* A hidden copy of the finished line holds the bubble at its final size, so
   nothing reflows while the words stream in. */
.cc__text { margin: 0; display: grid; font-family: var(--font-arabic);
  font-size: 17px; line-height: 1.75; color: var(--cc-t2);
  text-wrap: pretty; }
.cc__text > * { grid-area: 1 / 1; }
.cc__ghost { visibility: hidden; }
.cc__thinking { display: none; align-items: center; gap: 5px; }
.cc__thinking.is-on { display: flex; }
.cc__thinking i { width: 5px; height: 5px; border-radius: 3px;
  background: var(--cc-t4); }
.cc__chipwrap { margin: 0; display: flex; justify-content: flex-end;
  opacity: 0; transform: translateY(4px);
  transition: opacity .35s ease, transform .35s ease; }
.cc__chipwrap.is-on { opacity: 1; transform: none; }
.cc__chip { display: flex; align-items: center; gap: 8px; height: 28px;
  white-space: nowrap;
  padding: 0 10px; border-radius: 7px; background: var(--cc-s4);
  border: 1px solid var(--cc-l5); font-family: var(--font-mono);
  font-size: 12.5px; color: #cfcfd6; transition: border-color .4s; }
.cc__chip.is-done { border-color: #3a4a3d; }
.cc__spin { width: 10px; height: 10px; border-radius: 6px;
  border: 1.5px solid var(--cc-t6); border-top-color: #e6e6ea; }
.cc__check { display: none; width: 12px; height: 12px;
  color: var(--cc-ok); }
.cc__chip.is-done .cc__spin { display: none; }
.cc__chip.is-done .cc__check { display: block; }
.cc__result { color: var(--cc-t6); }

@media (max-width: 720px) {
  .cc { padding: 14px; }
}

/* Fewer and gentler, not none. The call still plays and the bubbles still
   arrive, because the arriving IS the content here; what goes is the travel
   and the three loops, which report state that the words and the labels
   already report. */
@media (prefers-reduced-motion: reduce) {
  .cc__row, .cc__chipwrap { transform: none; }
  .cc__pip.is-on, .cc__thinking i, .cc__spin { animation: none; }
}

/* ---------- Where leads are lost: the stage ----------
   Four problems banking away to the left, a phone lying on the grid, and what
   comes back out banking away to the right. PropIntel's funnel argument in
   isometric: many in, one core, a few out.

   The section has its own ground, a shade deeper than the page's wash, so the
   scene sits in a plane rather than floating on the body background. */
/* The section is class="lost". These rules targeted .signal, a class the
   markup stopped using when the cards went in, so every --sig-* token below
   was undeclared: each var() came back invalid, every stroke fell to none and
   the grid disappeared entirely. The phone's screen only looked right because
   an invalid fill defaults to black. */
.lost { --sig-band: color-mix(in srgb, var(--periwinkle-100) 55%, var(--bg));
  --sig-ink: var(--purple-1000);
  --sig-bad: color-mix(in srgb, var(--error-icon) 82%, var(--periwinkle-900));
  --sig-good: var(--accent-primary);
  background: var(--sig-band); }
main > section.lost + section { border-top: 0; }

/* The head sits at the RIGHT of the measure, under nothing and opposite the
   run of problems that comes down the left of the figure below it. Set left,
   it stacked its own weight on the side the drawing is already heaviest and
   left the top right of the section empty. The standfirst goes under the
   title rather than beside it: this is a column now, not a two column head. */
@media (min-width: 900px) {
  .lost .sechead__in { display: block; width: min(46ch, 100%);
    margin-inline-start: auto; }
  .lost .sechead .sec { max-width: 14ch; margin-bottom: var(--space-4); }
  .lost .sechead .lede { max-width: none; padding-top: 0; }
}

.stage__fig { position: relative; width: 100%; aspect-ratio: 1400 / 880;
  container-type: inline-size; margin-top: var(--space-8); }
.stage { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.stage line, .stage path, .stage circle { vector-effect: non-scaling-stroke;
  shape-rendering: geometricPrecision; stroke-linejoin: miter; }

/* The grid is gone. A dashed rectangle set out around the phone does the whole
   job it was doing: it sits flat, it is obviously a construction line rather
   than an edge of the object, and the phone reads as resting inside it. A full
   plane of lines was competing with the cards for the same attention. */
/* Two halves, each in the colour of the side that docks into it. The
   rectangle is the bus, so it is allowed to carry the argument's colour
   instead of staying a neutral construction line. */
.st-foot { fill: none; stroke-width: 1.25; stroke-dasharray: 5 6; }
.st-foot--in { stroke: color-mix(in srgb, var(--sig-bad) 58%, transparent); }
.st-foot--out { stroke: color-mix(in srgb, var(--sig-good) 58%, transparent); }

/* Legs run card to phone in three moves and no diagonals: straight down to
   the plane, along one ground axis, then the other. Dashed on the way in
   because a lost lead's route is inferred rather than recorded anywhere;
   solid on the way out because those are things the system did.
   No arrowheads. A head on a hairline at this size is a few pixels of noise,
   which is what made the old ones read as janky; the pin at the card end
   already says which way the line is going. */
.st-leg { fill: none; stroke-width: 1.25; stroke-linecap: round; }
.st-leg--in { stroke: var(--sig-bad); stroke-dasharray: 3 6; }
.st-leg--out { stroke: var(--sig-good); stroke-dasharray: 3 6; }
.st-pin { stroke: none; }
.st-pin--in { fill: var(--sig-bad); }
.st-pin--out { fill: var(--sig-good); }

/* Drawing a DASHED line on is the one thing stroke-dashoffset cannot do: the
   dash pattern is already spending that property. So each leg wears a mask cut
   from its own path string, stroked solid and wide, and the mask is what
   slides off. Every mask path carries pathLength="100", so one rule drives all
   nine regardless of how long the path it covers actually is.

   vector-effect has to be turned OFF here. The blanket rule at the top of the
   stage sets non-scaling-stroke on every path, which takes the dash pattern
   out of user units and breaks the percent that pathLength just bought.

   RESTING STATE IS DRAWN. dashoffset 0 means a browser with no scroll
   timelines, a narrow window and a reader who asked for less motion all get
   the finished scene rather than an empty one. */
.stage .st-draw { fill: none; stroke: #FFF; stroke-width: 10;
  stroke-linecap: round; stroke-linejoin: round; vector-effect: none;
  stroke-dasharray: 100; stroke-dashoffset: 0; }
/* The dot that does the drawing, riding the same path string its leg was cut
   from so it cannot leave the wire. Nothing at rest: at rest the line is
   already there and nothing is travelling along it. */
.st-run { opacity: 0; }
.st-run--in { fill: var(--sig-bad); }
.st-run--out { fill: var(--sig-good); }

/* The phone. Faces shaded top lightest through left to right darkest. */
.st-shadow { fill: color-mix(in srgb, var(--sig-ink) 13%, transparent); stroke: none; }
.st-body { stroke: color-mix(in srgb, var(--sig-ink) 55%, transparent); stroke-width: 1; }
.st-top   { fill: color-mix(in srgb, var(--sig-ink) 3%, #FFF); }
.st-left  { fill: color-mix(in srgb, var(--sig-ink) 20%, #FFF); }
.st-right { fill: color-mix(in srgb, var(--sig-ink) 34%, #FFF); }
.st-screen { fill: var(--sig-ink);
  stroke: color-mix(in srgb, var(--sig-ink) 55%, transparent); stroke-width: 1; }
.st-wave { stroke: var(--periwinkle-300); stroke-width: 2; stroke-linecap: round; }
.st-bar { stroke: none; }
.st-bar--0 { fill: color-mix(in srgb, var(--periwinkle-300) 30%, transparent); }
.st-bar--1 { fill: var(--accent-primary); }

/* ---------- The cards ----------
   Real containers with a title and a sentence, sitting where the brief puts
   them: up and away from the phone on both sides, each one landing on the
   anchor its own leg starts from. Upright rather than skewed into the
   projection: the arrangement carries the perspective, and skewed body copy
   in an isometric scene is unreadable at any size worth setting it at. */
.scards { list-style: none; margin: 0; padding: 0; }
/* Techy, minimal, a little moody, after the transfer panel reference: square
   corners, a hairline edge, mono caps for the name and no fill pretending to
   be a surface. Glass rather than paint, so the plane and the legs stay
   readable through the card and it sits IN the scene instead of on top of it.
   backdrop-filter is the one effect on this page that is a real optical one
   rather than a decoration. */
.scard { position: absolute; width: 19.5%; min-width: 172px;
  padding: var(--space-3) var(--space-4);
  font: var(--text-body);
  border-radius: 0;
  -webkit-backdrop-filter: blur(14px) saturate(135%);
  backdrop-filter: blur(14px) saturate(135%);
  /* An edge, not an elevation. A 1px border with a 26px diffuse shadow under
     it was both at once, which is the generated-UI tell, and a soft blur is
     the least technical thing that could sit on a page of hairline drawing.
     The card commits to the edge: the top rule already carries the meaning,
     and an opaque fill is what separates it from the grid behind. */
  background: color-mix(in srgb, #FFF 34%, transparent);
  border: 1px solid color-mix(in srgb, var(--sig-ink) 18%, transparent); }
.scard__t { margin: 0 0 var(--space-2); font: var(--text-caption);
  font-family: var(--font-mono); font-weight: 600; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ink); }
.scard__d { margin: 0; font: var(--text-body); color: var(--ink-secondary); }
/* The node is on the card's BOTTOM, at the corner the run is moving AWAY
   from: bottom right for the problems, which step down and left, bottom left
   for the outcomes, which step down and right.

   Centring the node on the bottom edge is the obvious thing and it is wrong.
   Both runs step diagonally, so a leg dropping from the middle of a card falls
   straight through the next two cards in its own run. Leaving from the outer
   corner puts every drop in clear air.

   Both runs are on the lattice, so every leg segment is either a straight
   drop or a true grid line. Off it, a leg had to turn at whatever angle
   reached the bus, and those angles are not in the projection. */
.scards .scard { left: calc(var(--x) * 1%); top: calc(var(--y) * 1%);
  margin-bottom: var(--space-2); }
.scards--in .scard { translate: -100% -100%; }
/* The outcomes step down and to the LEFT as they descend, so like the
   problems their leg leaves the bottom RIGHT corner, which is the side the run
   is moving away from. Hung the other way, the drop from each card fell
   straight through the two below it. */
.scards--out .scard { translate: -100% -100%; }
/* The only colour on the card, and it is carrying meaning rather than
   decorating: which side of the argument this one is on. */
.scards--in .scard { border-top: 2px solid var(--sig-bad); }
.scards--out .scard { border-top: 2px solid var(--sig-good); }

/* The card is part of the DRAWING, so it scales with the drawing.
   Everything below is a proportion of the figure's own width rather than a
   fixed size, because the anchors are percentages and the cards were not: the
   figure shrinking by a fifth left the same three lines of type in a run whose
   vertical steps had shrunk under them, and each card buried the one above.
   The reference is the figure at 1098px, where the type is 12 and 14.

   The floor stops it becoming a picture of some text. Under it the scene
   overflows the window it is pinned in by a few tens of pixels instead, which
   is the cheaper failure: the top and bottom of the frame are the quietest
   parts of the composition. */
@media (min-width: 900px) {
  .scard { font-size: max(11px, 1.275cqw); padding: 0.857em 1.143em; }
  .scard__t { font-size: 0.857em; line-height: 1.333; margin-bottom: 0.571em; }
  .scard__d { font-size: 1em; line-height: 1.429; }
}

@media (max-width: 899px) {
  /* The routing needs the width. Narrow, the scene goes and the cards stack. */
  .stage { display: none; }
  .stage__fig { aspect-ratio: auto; display: grid; gap: var(--space-4); }
  .scards { display: grid; gap: var(--space-4); }
  /* .scards .scard, not .scard: the anchors are set by .scards--in .scard
     and .scards--out .scard, and a one class selector here never beat them.
     Every card kept its translate of -100% -100% and hung a full card width
     off the left edge of the page with 24px of itself showing. */
  .scards .scard { position: static; width: auto; translate: none;
    margin: 0; }
}

/* ---------- Walkthrough ----------
   One third copy, two thirds port. The heading lives inside the left column
   rather than above the grid, so the port's top edge starts on the heading's
   line instead of below it. */
.watch__grid { display: grid; gap: var(--space-8); }
.watch__copy .sec { margin: 0 0 var(--space-4); max-width: 18ch; }
.watch__copy p { margin: 0; color: var(--ink-secondary); font: var(--text-body-large); }
@media (min-width: 960px) {
  .watch__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
    gap: var(--space-16); align-items: start; }
  /* The heading holds the port's top line and the standfirst drops to the foot
     of the column, so the short copy block spans the port's height instead of
     hanging under its own title with empty column beneath it. */
  .watch__copy { align-self: stretch; display: flex; flex-direction: column;
    justify-content: space-between; }
  .watch__copy p { max-width: 34ch; }
}
.reel { margin: 0; }
/* The port. Darker than its frame, with a hairline hatch across it so an empty
   16:9 rectangle reads as a screen awaiting a film rather than as a hole. */
.reel__stage { position: relative; display: grid; place-items: center;
  aspect-ratio: 16 / 9; border-radius: calc(var(--ht-cut) - 1px);
  background:
    repeating-linear-gradient(135deg,
      color-mix(in srgb, var(--periwinkle-200) 4%, transparent) 0 10px,
      transparent 10px 20px),
    var(--purple-1000); }
/* Not a control: there is nothing to press yet. It is the one cue that the
   frame stands in for a film. */
.reel__play { display: block; width: 4.5rem; height: 4.5rem; color: var(--periwinkle-200); }
.reel__play svg { display: block; width: 100%; height: 100%; }
/* Same size as the chrome bar's path above it, and the scale's own caption
   step. These arrived from PropIntel as .68rem and .69rem — two sizes a sixth
   of a pixel apart, which nobody can see and which makes the type scale look
   accidental rather than chosen. One value, and on the scale. */
.reel__label, .reel__time { position: absolute; bottom: var(--space-4); margin: 0;
  font: var(--text-caption); font-family: var(--font-mono); letter-spacing: 0;
  color: color-mix(in srgb, var(--periwinkle-200) 62%, transparent); }
.reel__label { left: var(--space-4); }
.reel__time { right: var(--space-4); }

/* ---------- What it does: two runs on an invisible rail ----------
   Six operations in two groups, hung off a line down the middle of the
   measure that is never drawn. The left run sets its text to that line and
   the right run sets its text to it from the other side, so six rules stop
   dead at the same x and the rail is the one thing in the section you can see
   without it being there.

   What was here first was a stack of rings threaded on a visible axis, with
   the call arriving at the top through a panel carrying the product name.
   That panel and its wire are gone, and with them the reason for the centre
   to be occupied at all: a rail you can see is a rail doing the work the
   alignment should be doing.

   The two runs are offset from each other on purpose. Level, three rows on
   the left sit opposite three rows on the right and read as three pairs,
   which is a relationship these six do not have. */
.doing__rail { display: grid; gap: var(--space-8);
  margin-top: var(--space-8); }
.doing__group { margin: 0 0 var(--space-6); font: var(--text-caption);
  font-family: var(--font-mono); letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ink-secondary); }
.doing__outs { list-style: none; margin: 0; padding: 0; display: grid;
  gap: var(--space-8); }
.doing__out { border-top: 1px solid var(--divider);
  padding-top: var(--space-3); }
@media (min-width: 1000px) {
  /* Pinned, these six rows have a whole window to hold. A rule with 12px
     under it in a run that has 500px to fill is a list crammed into the top
     of a frame it was given. */
  .doing__outs { gap: var(--space-16); }
  .doing__out { padding-top: var(--space-4); }
  .doing__group { margin-bottom: var(--space-8); }
}
.doing__t { font: var(--text-h3); margin: 0 0 var(--space-2); }
.doing__d { font: var(--text-body); color: var(--ink-secondary); margin: 0; }

@media (min-width: 1000px) {
  /* The head takes a third and the two runs take the other two. Above the
     rail it was a two column head with the standfirst off to the right, which
     put text on both sides of the measure and left the rail nothing of its
     own to sit in. Stacked in its own column, the head is one block and the
     rail has a field. */
  .doing__pin { display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
    gap: var(--space-16); align-items: start; }
  .doing .sechead { margin: 0; }
  .doing .sechead__in { display: block; }
  .doing .sechead .sec { max-width: 13ch; margin-bottom: var(--space-4); }
  .doing .sechead .lede { max-width: none; padding-top: 0; }

  .doing__rail { grid-template-columns: 1fr 1fr;
    gap: 0 var(--space-8); margin-top: 0; }
  /* Set to the rail from the right. Ranged right is harder to read than
     ranged left and it is the whole device here: without it the two runs are
     two columns with a gap between them, and there is no rail. */
  .doing__side--on { text-align: end; }
  .doing__side--on :is(.doing__group, .doing__out) {
    margin-inline-start: auto; }
  .doing__side--around { margin-top: var(--space-16); }
  /* A width, not a max width. Left to size to their own text the six rules
     came out six different lengths and only the rail edge lined up, so the
     outer edge of each run was ragged and the two runs sat at different
     distances from the edges of the measure. */
  .doing__out { width: min(34ch, 100%); }
}

/* ---------- Why it converts: four cards, each with its own mark ----------
   Four columns, each opening with a drawn mark. What was here was one very
   large keyword in accent purple per row, which did the compositional job
   honestly enough but said nothing: the word was the heading again, set
   bigger and in colour.

   A mark can say the thing instead. Three of these four reasons are about
   something the agent does NOT do, and in the duroob grammar a dashed line is
   exactly that: the answer it does not improvise, the length the call does
   not run to, the moment it does not carry on past. tools/make-whys.py draws
   them, and the one filled dot in each is where its sentence turns.

   The dot is also the only accent left in the section, which is the right
   amount: purple marking the point in a drawing, not a word set in colour. */
.whys { list-style: none; margin: 0; padding: 0; display: grid;
  gap: var(--space-4); }
.why { border: 1px solid var(--border); padding: var(--space-6);
  display: grid; align-content: start; }
/* Styled the way the sister products style theirs, because a reader should
   recognise the same marks across the family: the whole figure in the accent
   rather than ink with one part picked out, and weight coming from the
   hairline being non-scaling rather than from a thicker stroke.

   96px, which is PropIntel's own size for a mark that LEADS its column.
   e-tender runs 72 because its marks sit under a label and have to stay the
   card's middle weight; with the keyword gone from the top of these cards
   there is nothing above the mark to be quieter than, so it takes the room.

   Set to the same edge as the title and the sentence. Centred it was the one
   thing in the card on a different axis, which is what reads as the card
   being out of alignment even when every box in it is where it should be. */
.why__m { display: block; width: 96px; height: 96px;
  color: var(--accent-primary);
  margin: var(--space-2) 0 var(--space-6); }
.why__m svg { display: block; width: 100%; height: 100%; }
.mk-dot { fill: currentColor; }
.why__t { font: var(--text-h3); letter-spacing: -0.01em;
  margin: 0 0 var(--space-2); }
.why__d { font: var(--text-body); color: var(--ink-secondary); margin: 0; }

@media (min-width: 900px) {
  /* Four across. The cards stretch to the tallest, and the content is held
     at the top of each so the four marks sit on one line and the four titles
     sit on another: the row reads across before it reads down. */
  .whys { grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-4); }
}

/* ---------- Close ---------- */
.close { text-align: center; }
.close h2 { font: var(--text-h1); letter-spacing: -0.025em; margin: 0 0 var(--space-4);
  text-wrap: balance; }
@media (max-width: 900px) { .close h2 { font: var(--text-h2); letter-spacing: -0.015em; } }
.close p { font: var(--text-body-large); color: var(--ink-secondary);
  margin: 0 auto var(--space-8); max-width: 46ch; }

/* ---------- Footer ----------
   Two things on one line, then one line under them. The nav above already goes
   everywhere this page goes, so the footer carries no links. */
.foot { padding-block: var(--space-8); border-top: 1px solid var(--divider); }
.foot__in { display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-8);
  align-items: center; justify-content: space-between; }
.foot__mark { display: flex; flex-direction: column; gap: var(--space-1); }
.foot__lockup { height: 26px; width: auto; }
.foot__label { font: var(--text-caption); color: var(--ink-secondary); margin: 0;
  text-transform: uppercase; letter-spacing: 0.14em; }
.foot p.foot__note { font: var(--text-caption); color: var(--ink-secondary);
  margin: 0; text-align: end; }
/* The copyright is a machine-produced string, and it belongs to the page rather
   than to either side of the row above, so it sits centred under both. */
.foot__copy { font: var(--text-caption); font-family: var(--font-mono); letter-spacing: 0;
  color: var(--ink-secondary); text-align: center;
  margin: var(--space-6) 0 0; padding-top: var(--space-6);
  border-top: 1px solid var(--divider); }

/* ---------- Motion ----------
   Every rule lives inside @supports and a reduced-motion query, and the RESTING
   state is the finished one — so an unsupporting browser and a reader who asked
   for less motion both get the whole page rather than an empty one. */
.page .dr-btn { transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1); }
.page .dr-btn:active { transform: translateY(1px); }

@keyframes ht-rise { from { opacity: 0; translate: 0 16px; } to { opacity: 1; translate: 0 0; } }
@keyframes ht-in { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: no-preference) {
  /* The hero plays on load. An exponential ease-out from an already-visible
     default, so a slow connection never leaves the page blank. */
  .hero__copy > *, .hero .viewport {
    animation: ht-rise 620ms cubic-bezier(0.16, 1, 0.3, 1) both; }
  .hero__copy > :nth-child(1) { animation-delay: 60ms; }
  .hero__copy > :nth-child(2) { animation-delay: 140ms; }
  .hero__copy > :nth-child(3) { animation-delay: 220ms; }
  .hero .viewport { animation-delay: 300ms; }

  /* ---------- The call card's own motion ----------
     Three loops, all of them reporting something that is genuinely running:
     the pip while a call is up, the dots while the agent is thinking, the
     ring while the lookup is out. Each one stops when the thing it reports
     stops, which is why they are allowed to loop at all.

     The card's entrances are TRANSITIONS on state classes, not keyframes,
     and they live with the component above: they are triggered by a script
     that can be interrupted mid call, and a transition retargets from wherever
     it is while a keyframe restarts from zero. */
  @keyframes cc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
  @keyframes cc-bounce { 0%, 80%, 100% { opacity: .25; translate: 0 0; }
                         40% { opacity: 1; translate: 0 -3px; } }
  @keyframes cc-spin { to { rotate: 360deg; } }
  .cc__pip.is-on { animation: cc-pulse 1.6s ease-in-out infinite; }
  .cc__thinking i { animation: cc-bounce 1.1s ease-in-out infinite; }
  .cc__thinking i:nth-child(2) { animation-delay: .15s; }
  .cc__thinking i:nth-child(3) { animation-delay: .3s; }
  .cc__spin { animation: cc-spin .8s linear infinite; }

}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* One authored moment, not the same entrance on every section. The page
       had .card, .why and the list rows all rising 16px identically,
       which is four sections agreeing to do nothing in particular.

       The stage below holds still and assembles itself while it is pinned.
       The video port wipes down its own height, because it is a screen.
       Nothing else animates in: the two groups and the four reasons are read,
       not performed, and a page where everything enters has no entrances. */

    /* ---------- The stage assembles ----------
       Eleven beats on ONE timeline, in the order the argument runs: the
       rectangle is set out, the phone lands inside it, four problems arrive
       and each drops its route into the left side of the bus, the screen
       answers, then three outcomes leave from the right side and land as
       their own routes reach them.

       Every range is a percentage of the same pinned runway, so the sequence
       is one number line and two beats cannot drift into each other by
       accident. --s is the index the generator drew each thing in, so the
       order on screen is the order in the file.

       The scene is gated on width as well as on motion: below 900px the
       isometric routing is dropped for a stack, and there is nothing to
       assemble. */
    @media (min-width: 1000px) {
      /* A runway of empty scroll after the section, and the head and figure
         stuck to the middle of the window for the length of it. The same
         device e-tender's cycle rail and PropIntel's funnel use, and the
         reason this can take eleven beats without the reader scrolling past
         nine of them.

         Pinning lives in here with the scrub rather than outside it: a
         section that holds still for 130vh and then does nothing is worse
         than a section that never held still. */
      .stage__stage { --runway: 150vh; view-timeline: --ht-stage block; }
      .stage__stage::after { content: ""; display: block; height: var(--runway); }
      .stage__pin { position: sticky; top: 0; min-height: 100vh;
        display: grid; align-content: center; }
      /* Pinned, the figure is bound by the WINDOW as well as by the measure:
         at full width its own bottom corner scrolls out of the frame it is
         being held in. 216px is the head above it plus the two gaps around
         it, measured rather than guessed, and the floor is the width under
         which the cards stop being readable. */
      .stage__fig { width: clamp(880px, calc((100vh - 216px) * 1400 / 880), 100%);
        margin-top: var(--space-4); margin-inline: auto; }
      .lost .sechead { margin-bottom: var(--space-4); }

      @keyframes st-draw-on { from { stroke-dashoffset: 100; }
                              to   { stroke-dashoffset: 0; } }
      /* An outcome leaves the phone, so its leg draws from the BUS end. That
         is where its path string finishes, so the same dash runs off the
         other way. */
      @keyframes st-draw-back { from { stroke-dashoffset: -100; }
                                to   { stroke-dashoffset: 0; } }
      /* transform, not translate: the cards are POSITIONED with translate
         (-100% -100% hangs each one off its own node) and an entrance written
         in the same property would throw them off their anchors. */
      @keyframes st-land-l { from { opacity: 0; transform: translate(-30px, -8px); }
                             to   { opacity: 1; transform: none; } }
      @keyframes st-land-r { from { opacity: 0; transform: translate(30px, -8px); }
                             to   { opacity: 1; transform: none; } }
      @keyframes st-rise { from { opacity: 0; transform: translateY(26px); }
                           to   { opacity: 1; transform: none; } }
      @keyframes st-pop { from { transform: scale(0); } to { transform: none; } }
      /* The dot IS the drawing: it rides the leg over exactly the range the
         mask uncovers it, so the line appears behind the dot rather than
         beside it. */
      @keyframes st-ride { from { offset-distance: 0%; opacity: 0; }
                           8%   { opacity: 1; }
                           92%  { opacity: 1; }
                           to   { offset-distance: 100%; opacity: 0; } }
      @keyframes st-ride-back { from { offset-distance: 100%; opacity: 0; }
                                8%   { opacity: 1; }
                                92%  { opacity: 1; }
                                to   { offset-distance: 0%; opacity: 0; } }
      @keyframes st-heard { from { opacity: 0; transform: scale(0.2); }
                            to   { opacity: 1; transform: none; } }

      .stage__stage :where(.scard, .st-draw, .st-run, .st-pin, .st-phone,
                           .st-wave) {
        animation-timeline: --ht-stage; animation-timing-function: linear;
        animation-fill-mode: both; }
      .st-pin, .st-wave { transform-box: fill-box; transform-origin: center; }

      /* 1. The rectangle is set out. Both halves draw from the top corner. */
      .st-draw--foot { animation-name: st-draw-on;
        animation-range: contain 2% contain 15%; }
      /* 2. The phone lands in it. */
      .st-phone { animation-name: st-rise;
        animation-range: contain 8% contain 20%; }
      /* 3. Four problems arrive from the left, each one dropping its route
            into the bus a beat after it lands. */
      .scards--in .scard { animation-name: st-land-l;
        animation-range: contain calc(20% + var(--s) * 8%)
                         contain calc(28% + var(--s) * 8%); }
      .st-pin--in { animation-name: st-pop;
        animation-range: contain calc(25% + var(--s) * 8%)
                         contain calc(29% + var(--s) * 8%); }
      .st-draw--in { animation-name: st-draw-on; }
      .st-run--in { animation-name: st-ride; }
      .st-draw--in, .st-run--in {
        animation-range: contain calc(27% + var(--s) * 8%)
                         contain calc(37% + var(--s) * 8%); }
      /* 4. The screen answers, filling across the way a voice does. */
      .st-wave { animation-name: st-heard;
        animation-range: contain calc(60% + var(--i) * 0.6%)
                         contain calc(64% + var(--i) * 0.6%); }
      /* 5. Three outcomes leave from the right side of the bus, and each card
            lands as its own route reaches it. */
      .st-draw--out { animation-name: st-draw-back; }
      .st-run--out { animation-name: st-ride-back; }
      .st-draw--out, .st-run--out {
        animation-range: contain calc(68% + var(--s) * 7%)
                         contain calc(78% + var(--s) * 7%); }
      .scards--out .scard { animation-name: st-land-r;
        animation-range: contain calc(76% + var(--s) * 7%)
                         contain calc(84% + var(--s) * 7%); }
      .st-pin--out { animation-name: st-pop;
        animation-range: contain calc(80% + var(--s) * 7%)
                         contain calc(84% + var(--s) * 7%); }
    }

    /* One side of the rail, then the other. The left run is what happens
       while the call is up and the right run is what happens around it, so
       the order they arrive in is the order the two things happen in, and
       the break between the two groups is long enough to read as a break
       rather than as a slow stagger.

       Both runs arrive FROM their own side, away from the rail, so the rail
       is the thing they settle against.

       ONE NAMED TIMELINE, on the section. With animation-timeline: view() on
       each row, every row runs off its OWN arrival in the window, so the
       ranges below would shift six independent animations instead of laying
       out one sequence: the right run would start before the left run had
       finished simply because it sits lower on the page. --s is -1 on the
       group name so each label leads its own three rows. */
    @keyframes ht-rail-l { from { opacity: 0; translate: -26px 0; }
                           to   { opacity: 1; translate: 0 0; } }
    @keyframes ht-rail-r { from { opacity: 0; translate: 26px 0; }
                           to   { opacity: 1; translate: 0 0; } }
    @media (min-width: 1000px) {
      /* Pinned while the two runs arrive, the same device the stage uses.
         Unpinned, the whole sequence played out over the section's own
         arrival in the window, which meant the right hand run was already
         landing while the left was still coming in from the bottom of the
         screen. */
      .doing__stage { --runway: 80vh; view-timeline: --ht-doing block; }
      .doing__stage::after { content: ""; display: block;
        height: var(--runway); }
      /* The pin fills the window and the runs are spaced to fill the pin.
         At the rail's first rhythm the two runs came to 363px centred in
         900, which left 277px of dead band above them and 277 below and read
         as a hole; the answer was not a shorter window but a run that earns
         the one it is in. */
      .doing__pin { position: sticky; top: 0; min-height: 100vh;
        align-content: center; }
      .doing__group { --s: -1; }
      .doing__group, .doing__out {
        animation-timeline: --ht-doing; animation-timing-function: linear;
        animation-fill-mode: both; }
      .doing__side--on :is(.doing__group, .doing__out) {
        animation-name: ht-rail-l;
        animation-range: contain calc(8% + (var(--s) + 1) * 9%)
                         contain calc(20% + (var(--s) + 1) * 9%); }
      .doing__side--around :is(.doing__group, .doing__out) {
        animation-name: ht-rail-r;
        animation-range: contain calc(50% + (var(--s) + 1) * 9%)
                         contain calc(62% + (var(--s) + 1) * 9%); }
    }

    /* ---------- The four marks, in PropIntel's order ----------
       Its trust cards run head, then the marks one at a time, then the words
       under them, and each mark's own parts arrive rather than the whole
       thing fading in at once. Same order here, and the same reason: a mark
       that only fades is a picture of a mark, and these are built out of
       parts that can be seen to arrive.

       PropIntel drives this from js/motion.js off an IntersectionObserver.
       This page has no motion driver, so the same sequence is laid out on one
       named timeline. The effect is the argument, not the mechanism.

       The marks are revealed WHOLE rather than drawn on, for the reason
       PropIntel gives: drawing overwrites stroke-dasharray, and two of these
       carry a dashed halo that is part of the drawing. */
    .edge { view-timeline: --ht-edge block; }
    .why:nth-child(1) { --i: 0; }
    .why:nth-child(2) { --i: 1; }
    .why:nth-child(3) { --i: 2; }
    .why:nth-child(4) { --i: 3; }

    @keyframes ht-rise-m { from { opacity: 0; translate: 0 10px; }
                           to   { opacity: 1; translate: 0 0; } }
    /* The mark rises into its own space as the section comes up the window.
       Further than the words under it travel, 28px against their 10, so the
       four of them read as the thing arriving and the text as settling after
       it. A reveal from the top edge down was the other reading of this and
       it is not what happens: they rise. */
    @keyframes ht-mark-rise { from { opacity: 0; translate: 0 28px; }
                              to   { opacity: 1; translate: 0 0; } }
    @keyframes ht-grow { from { transform: scale(.88); } to { transform: none; } }
    @keyframes ht-bars { from { transform: scaleY(.3); } to { transform: none; } }
    @keyframes ht-swing { from { transform: rotate(-46deg); }
                          to   { transform: none; } }
    @keyframes ht-face { from { opacity: 0; transform: scale(.9); }
                         to   { opacity: 1; transform: none; } }
    @keyframes ht-close { from { transform: translateY(var(--from)); }
                          to   { transform: none; } }

    .edge .sechead, .why__m, .why__t, .why__d,
    .mk-halo, .mk-p1, .mk-p2 {
      animation-timeline: --ht-edge; animation-timing-function: linear;
      animation-fill-mode: both; }
    /* A transform on an SVG child pivots at the user space origin unless it
       is told otherwise, which would swing these about the corner of the
       viewBox instead of their own centre. */
    .mk-halo, .mk-p1, .mk-p2 { transform-box: view-box;
      transform-origin: 40px 40px; }

    /* COVER, not ENTRY. Entry runs from the section's top edge reaching the
       bottom of the window to its own bottom edge reaching the same line, and
       this section is 630px in an 880px window: at entry 30% the cards are
       still below the fold, so the whole sequence played out under the screen
       and the reader arrived to find it finished. Cover spans the full time
       any of the section is in view, so these numbers are where the cards
       actually are while they animate. */
    .edge .sechead { animation-name: ht-rise-m;
      animation-range: cover 16% cover 24%; }
    .why__m { animation-name: ht-mark-rise;
      animation-range: cover calc(24% + var(--i) * 4%)
                       cover calc(34% + var(--i) * 4%); }
    .why__t, .why__d { animation-name: ht-rise-m;
      animation-range: cover calc(28% + var(--i) * 4%)
                       cover calc(38% + var(--i) * 4%); }
    /* The halo settles a beat after the mark inside it, so the ring reads as
       closing around the thing rather than arriving with it. */
    .mk-halo { animation-name: ht-grow;
      animation-range: cover calc(27% + var(--i) * 4%)
                       cover calc(38% + var(--i) * 4%); }
    .mk-p1, .mk-p2 {
      animation-range: cover calc(26% + var(--i) * 4%)
                       cover calc(37% + var(--i) * 4%); }
    /* Each mark's parts do the thing that mark is about: the voice opens out
       of the axis, the cube builds its faces onto its silhouette, the pair on
       the loop swings round into place, and the two planes close together. */
    [data-mark="01"] .mk-p1 { animation-name: ht-grow; }
    [data-mark="01"] .mk-p2 { animation-name: ht-bars; }
    [data-mark="02"] .mk-p1 { animation-name: ht-grow; }
    [data-mark="02"] .mk-p2 { animation-name: ht-face; }
    [data-mark="03"] .mk-p1 { animation-name: ht-swing; }
    [data-mark="04"] .mk-p1 { animation-name: ht-close; --from: -8px; }
    [data-mark="04"] .mk-p2 { animation-name: ht-close; --from: 8px; }

    @keyframes ht-wipe { from { clip-path: inset(0 0 100% 0); }
                         to   { clip-path: inset(0 0 0 0); } }
    .reel { animation: ht-wipe linear both; animation-timeline: view();
      animation-range: entry 12% cover 14%; }

  }
}
