@font-face {
  font-family: "Space Grotesk";
  src: url("assets/fonts/space-grotesk-latin.woff2") format("woff2");
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("assets/fonts/jetbrains-mono-latin.woff2") format("woff2");
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
}

:root {
  color-scheme: light;
  --black: #0d0d0d;
  --paper: #f5f2eb;
  --cobalt: #2b5fe3;
  --cobalt-hover: #4b7bff;
  --cobalt-light: #6e97ff;
  --orange: #f15b22;
  --yellow: #ffc613;
  --green: #1e7a44;
  --ink: #17160f;
  --logo-neutral: #1a1813;
  --dur-brand-strobe: 680ms;
  --brand-strobe-iterations: 3;
  --n-800: #2e2a20;
  --n-700: #57534a;
  --n-600: #6e6959;
  --n-500: #837d6e;
  --n-400: #a49e8c;
  --n-300: #cfc9ba;
  --n-250: #d8d2c4;
  --n-200: #e2ddd0;
  --n-150: #ede9df;
  --n-100: #f1ede3;
  --n-50: #faf8f2;
  --surface-card: #fff;
  --dark-panel: #17160f;
  --dark-code: #141414;
  --dark-hairline: #1e1e1c;
  --dark-border: #33332c;
  --dark-text: #f1ede3;
  --dark-muted: #b8b3a7;
  --dark-faint: #8f8b80;
  --dark-dim: #6f6b5f;
  --syn-keyword: #6e97ff;
  --syn-function: #ff8a5b;
  --syn-string: #ffc613;
  --syn-number: #b9e08c;
  --syn-comment: #6e6a60;
  --syn-plain: #cfc9bb;
  --font-display: "Space Grotesk", "Helvetica Neue", sans-serif;
  --font-mono: "JetBrains Mono", monospace;
  --radius-sm: 6px;
  --radius-btn: 8px;
  --radius-menu: 10px;
  --radius-card: 12px;
  --radius-lg: 14px;
  --radius-panel: 16px;
  --radius-pill: 999px;
  --shadow-raised: 0 1px 2px rgba(20, 18, 12, 0.04), 0 10px 26px rgba(20, 18, 12, 0.06);
  --shadow-overlay: 0 18px 44px rgba(20, 18, 12, 0.16);
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --dur-instant: 120ms;
  --dur-default: 200ms;
  --dur-considered: 320ms;
  /* One size for every heading that is not the cover. The cover is the only
     hero in the deck; section headings share this so no other slide competes
     with it. */
  --section-type: clamp(38px, 3.6vw, 58px);
  --slide-x: clamp(56px, 6.2vw, 100px);
  --slide-y: clamp(30px, 4.2vh, 52px);
  /* Band the slide footer sits in, reserved out of every slide's box. */
  --slide-footer: clamp(34px, 4vh, 46px);
}

* { box-sizing: border-box; }

/* Paper behind the slide stack: any subpixel seam between slides blends into
   the same tone instead of exposing a dark 1-2px sliver at the dividers. */
html {
  scroll-behavior: smooth;
  background: var(--paper);
}

body {
  margin: 0;
  overflow-x: hidden;
  color: var(--ink);
  background: var(--paper);
  font-family: var(--font-display);
  -webkit-font-smoothing: antialiased;
}

button, a { font: inherit; }
button:focus-visible, a:focus-visible { outline: 3px solid var(--cobalt); outline-offset: 3px; }

.skip-link {
  position: fixed;
  z-index: 100;
  top: 12px;
  left: 12px;
  padding: 10px 14px;
  color: #fff;
  background: var(--ink);
  transform: translateY(-160%);
}
.skip-link:focus { transform: translateY(0); }

.deck { width: 100%; }

.slide {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  overflow: hidden;
  padding: var(--slide-y) var(--slide-x) calc(var(--slide-y) + var(--slide-footer));
  background: var(--paper);
  isolation: isolate;
}

/* Landing band rhythm: white bands alternate with the warm paper page,
   and every band boundary carries the site's 1px hairline. */
.slide:nth-child(odd) { background: var(--surface-card); }
.slide + .slide { border-top: 1px solid var(--n-200); }

.slide-chrome {
  position: relative;
  z-index: 8;
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 38px;
}
/* The cover has no section label, so without this the number is the only
   child and space-between leaves it on the left. */
.slide-chrome .slide-number { margin-left: auto; }
/* Mark and contact, at the foot of every slide. Injected by deck.js rather
   than repeated nine times in the markup. */
.slide-footer {
  position: absolute;
  z-index: 8;
  right: var(--slide-x);
  bottom: var(--slide-y);
  left: var(--slide-x);
  display: flex;
  align-items: center;
  /* Both left: the deck controls are fixed to the bottom-right corner and
     would sit on top of a right-aligned contact on every slide. */
  justify-content: flex-start;
  gap: clamp(16px, 1.9vw, 28px);
}
.brand-mark { display: inline-flex; line-height: 0; }
.brand-mark svg { width: clamp(152px, 13vw, 206px); height: auto; }
a.slide-mail {
  color: var(--cobalt);
  text-decoration: none;
  text-transform: none;
  letter-spacing: 0.04em;
}
a.slide-mail:hover { color: var(--cobalt-hover); }
/* Quieter than the contact: it is a disclosure, not a call to action. Left
   with the rest, not pushed right: the deck controls are fixed to the
   bottom-right corner and would sit on top of it on every slide. */
a.slide-privacy {
  color: var(--n-500);
  text-decoration: none;
  text-transform: none;
  letter-spacing: 0.04em;
}
a.slide-privacy:hover { color: var(--ink); }

/* Hover strobe, ported from the landing's Logo.astro. Each facet runs its own
   keyframe so the mark reshuffles rather than flashing as one block, and the
   animated property is `fill` on a light-DOM path: animating a custom property
   instead lets Chromium invalidate it and flatten every facet to one colour. */
@keyframes logo-f1 { 0%, 100% { fill: var(--logo-neutral); } 35% { fill: var(--cobalt); } 50% { fill: var(--orange); } 85% { fill: var(--yellow); } }
@keyframes logo-f2 { 0%, 100% { fill: var(--orange); } 35% { fill: var(--yellow); } 50% { fill: var(--logo-neutral); } 85% { fill: var(--cobalt); } }
@keyframes logo-f3 { 0%, 100% { fill: var(--yellow); } 35% { fill: var(--logo-neutral); } 50% { fill: var(--cobalt); } 85% { fill: var(--orange); } }
@keyframes logo-f4 { 0%, 100% { fill: var(--cobalt); } 35% { fill: var(--orange); } 50% { fill: var(--yellow); } 85% { fill: var(--logo-neutral); } }
@keyframes logo-f5 { 0%, 100% { fill: var(--orange); } 35% { fill: var(--yellow); } 50% { fill: var(--cobalt); } 85% { fill: var(--logo-neutral); } }
@keyframes logo-f6 { 0%, 100% { fill: var(--logo-neutral); } 35% { fill: var(--cobalt); } 50% { fill: var(--orange); } 85% { fill: var(--yellow); } }
@keyframes logo-f7 { 0%, 100% { fill: var(--cobalt); } 35% { fill: var(--orange); } 50% { fill: var(--yellow); } 85% { fill: var(--cobalt); } }
@keyframes logo-f8 { 0%, 100% { fill: var(--yellow); } 35% { fill: var(--logo-neutral); } 50% { fill: var(--cobalt); } 85% { fill: var(--orange); } }
@keyframes logo-f9 { 0%, 100% { fill: var(--yellow); } 35% { fill: var(--cobalt); } 50% { fill: var(--logo-neutral); } 85% { fill: var(--yellow); } }

/* Name stays unset until hover: dropping it on exit gives the next hover a
   fresh animation instance rather than a resumed one. */
.logo-facet--1 { --logo-animation: logo-f1; }
.logo-facet--2 { --logo-animation: logo-f2; }
.logo-facet--3 { --logo-animation: logo-f3; }
.logo-facet--4 { --logo-animation: logo-f4; }
.logo-facet--5 { --logo-animation: logo-f5; }
.logo-facet--6 { --logo-animation: logo-f6; }
.logo-facet--7 { --logo-animation: logo-f7; }
.logo-facet--8 { --logo-animation: logo-f8; }
.logo-facet--9 { --logo-animation: logo-f9; }

@media (hover: hover) {
  .brand-mark:hover .logo-facet {
    animation-name: var(--logo-animation);
    animation-duration: var(--dur-brand-strobe);
    animation-timing-function: steps(1, end);
    animation-iteration-count: var(--brand-strobe-iterations);
    animation-fill-mode: both;
  }
}

.section-label,
.slide-number,
.slide-mail,
.mono-label,
.step-number,
.founder-head span {
  margin: 0;
  color: var(--n-500);
  font-family: var(--font-mono);
  font-size: clamp(10px, 0.75vw, 12px);
  font-weight: 600;
  letter-spacing: 0.15em;
  line-height: 1.2;
  text-transform: uppercase;
}

.slide-heading,
.team-heading {
  position: relative;
  z-index: 3;
  max-width: 1120px;
  margin-top: clamp(38px, 5.2vh, 62px);
}

.compact-heading { margin-top: clamp(30px, 4vh, 48px); }

.slide-heading h2,
.team-heading h2 {
  margin: 0;
  font-size: var(--section-type);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1;
}

.slide-heading h2 > span { display: block; }

.slide-heading > p,
.team-heading > p {
  max-width: 850px;
  margin: 20px 0 0;
  color: var(--n-700);
  font-size: clamp(17px, 1.45vw, 23px);
  line-height: 1.5;
  /* Guards the last line: without it the problem lede strands "the industry."
     on a 116px third line. */
  text-wrap: pretty;
}

.ds-card {
  background: var(--surface-card);
  border: 1px solid var(--n-200);
  border-radius: var(--radius-lg);
}

.dark-card {
  color: var(--dark-text);
  background: var(--dark-panel);
  border: 1px solid #23231f;
  border-radius: var(--radius-panel);
}


/* Cover: current homepage hero language */

/* Cover: the brand pattern field, one quiet tone. */
.cover { overflow: hidden; }

.hero-pattern-field {
  --hero-pattern-size: 144px;
  --hero-pattern-edge-trim: 6px;
  position: absolute;
  z-index: 0;
  inset: calc(0px - var(--hero-pattern-edge-trim)) 0;
  display: block;
  overflow: hidden;
  pointer-events: none;
  background: var(--surface-card);
}

.hero-pattern-tiles {
  position: absolute;
  inset: 0;
  display: block;
  overflow: hidden;
  opacity: 0;
  transition: opacity var(--dur-default) var(--ease-standard);
}
.hero-pattern-field[data-pattern-visible="true"] .hero-pattern-tiles { opacity: 1; }

.hero-pattern-tile {
  position: absolute;
  display: block;
  width: calc(var(--hero-pattern-size) + var(--hero-pattern-tile-overlap, 0px));
  height: calc(var(--hero-pattern-size) + var(--hero-pattern-tile-overlap, 0px));
  background: var(--n-50);
  -webkit-mask-image: var(--hero-pattern-tile-image);
  mask-image: var(--hero-pattern-tile-image);
  -webkit-mask-mode: alpha;
  mask-mode: alpha;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

@media (max-width: 1020px) {
  .hero-pattern-field { --hero-pattern-size: 124px; --hero-pattern-edge-trim: 5px; }
}

/* Cover figure: eight incompatible source formats piled on the left, resolving
   through one arrow into one dataset card. Sits directly above the copy, a
   fixed step wider than the headline and centred on it. */
.cover-figure {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: clamp(16px, 2vw, 34px);
  width: var(--fig-measure);
  height: clamp(150px, 35vh, 320px);
  pointer-events: none;
}

/* The mess */
.fig-mess { position: relative; flex: 1 1 auto; height: 100%; }
.fig-chip {
  position: absolute;
  left: var(--l);
  top: var(--t);
  /* Fluid, and set large: the labels carry the field, so the gaps between
     them close by growing the chips rather than by crowding the scatter. */
  padding: clamp(8px, 0.75vw, 13px) clamp(13px, 1.25vw, 22px);
  border-radius: 10px;
  font: 500 clamp(10.5px, 0.95vw, 14px)/1 var(--font-mono);
  font-style: normal;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  transform: translate(-50%, -50%) rotate(var(--r));
}
.fig-chip.cobalt { color: #fff; background: var(--cobalt); }
.fig-chip.orange { color: #fff; background: var(--orange); }
.fig-chip.yellow { color: var(--ink); background: var(--yellow); }
.fig-chip.ink { color: var(--paper); background: var(--ink); }

/* The turn: the pile resolves into the card beside it. The extra right margin
   sits it nearer the midpoint of the run: the chips stop short of the field's
   right edge, so an evenly gapped arrow reads as hugging the card. Quiet on
   purpose, it is the connector between the two halves, not a third object. */
.fig-arrow {
  flex: none;
  width: auto;
  height: clamp(24px, 3.4vh, 40px);
  margin-right: clamp(20px, 3.3vw, 52px);
  overflow: visible;
}
.fig-arrow path {
  fill: none;
  stroke: var(--ink);
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

/* The resolved dataset: one card holding the four domains the subline names,
   drawn with the landing page's own gtile idiom (rounded colour square, brand
   dataset glyph knocked out of it) and the design system's card treatment:
   white on a hairline, soft radius, no resting shadow. */
.fig-card {
  position: relative;
  flex: none;
  align-self: center;
  display: flex;
  flex-direction: column;
  gap: clamp(11px, 1.2vw, 16px);
  height: 100%;
  aspect-ratio: 1.02;
  padding: clamp(13px, 1.4vw, 18px);
  background: var(--surface-card);
  border: 1px solid var(--n-200);
  border-radius: var(--radius-lg);
}
.fig-swatches {
  display: grid;
  flex: 1 1 auto;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: clamp(7px, 0.8vw, 11px);
  min-height: 0;
}
/* Landing-page gtile: a rounded colour square carrying one dataset glyph. */
.gtile {
  position: relative;
  border-radius: var(--radius-menu);
}
.gtile::after {
  content: "";
  position: absolute;
  inset: 22%;
  background: var(--gtile-glyph);
  -webkit-mask-image: var(--gtile-shape);
  mask-image: var(--gtile-shape);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
.gtile.on-cobalt { background: var(--cobalt); --gtile-glyph: var(--n-100); }
.gtile.on-yellow { background: var(--yellow); --gtile-glyph: var(--ink); }
.gtile.on-ink { background: var(--ink); --gtile-glyph: var(--n-100); }
.gtile.on-orange { background: var(--orange); --gtile-glyph: var(--ink); }
/* Every shape here is the brand's own dataset glyph for that domain, taken
   from the landing page's symbols.json mapping rather than picked by eye. */
.gtile.g-weather { --gtile-shape: url("assets/brand/glyph-weather.svg"); }
.gtile.g-economic { --gtile-shape: url("assets/brand/glyph-economic.svg"); }
.gtile.g-reports { --gtile-shape: url("assets/brand/glyph-reports.svg"); }
.gtile.g-satellite { --gtile-shape: url("assets/brand/glyph-satellite.svg"); }
.gtile.g-ghcnh { --gtile-shape: url("assets/brand/glyph-ghcnh.svg"); }
.gtile.g-asos { --gtile-shape: url("assets/brand/glyph-asos.svg"); }
.gtile.g-nwscli { --gtile-shape: url("assets/brand/glyph-nwscli.svg"); }
.gtile.g-training { --gtile-shape: url("assets/brand/glyph-training.svg"); }
.gtile.g-regulatory { --gtile-shape: url("assets/brand/glyph-regulatory.svg"); }
.gtile.g-markets { --gtile-shape: url("assets/brand/glyph-markets.svg"); }
/* The tiles turn over with the sources, on the same fade. */
.gtile { transition: opacity 0.26s var(--ease-standard); }
.gtile.is-swapping { opacity: 0; }
/* The caption: what the set is called, and what it took to make it. Stacked
   so the longest dataset name still gets the card's full width. */
.fig-label {
  display: flex;
  flex: none;
  flex-direction: column;
  gap: clamp(4px, 0.45vw, 6px);
}
.fig-label b {
  color: var(--ink);
  font: 500 clamp(8.5px, 0.74vw, 10.5px)/1 var(--font-mono);
  letter-spacing: 0.01em;
  white-space: nowrap;
  transition: opacity 0.26s var(--ease-standard);
}
.fig-label b.is-swapping { opacity: 0; }
.fig-label span {
  color: var(--n-500);
  font: 500 clamp(7px, 0.62vw, 8.5px)/1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
/* left/top glide far slower than the fade, so a chip is already travelling to
   its new spot as it comes back in: the pile rearranges rather than being
   swapped out. Colour eases too, since the tones change with the dataset. */
.fig-chip {
  transition:
    left 0.55s var(--ease-standard),
    top 0.55s var(--ease-standard),
    background-color 0.4s var(--ease-standard),
    color 0.4s var(--ease-standard),
    transform 0.2s var(--ease-standard),
    opacity 0.2s var(--ease-standard);
}
.fig-chip.is-swapping { opacity: 0; transform: translate(-50%, -50%) rotate(var(--r)) scale(0.82); }
/* Sets carry 6-8 sources; the surplus chips stay in the DOM and just leave. */
.fig-chip.is-out { opacity: 0; transform: translate(-50%, -50%) rotate(var(--r)) scale(0.7); }

/* Copy at the foot of the slide, the figure directly above it, both on one
   measure. --hero-measure is the headline's own rendered line width: at Space
   Grotesk 700 / -0.05em the two lines set 9.87x and 9.86x their font size, so
   9.87 x --hero-type is exactly how wide the headline draws. Everything else
   derives from that, which is what keeps the figure the same width as the
   words no matter what the viewport does. */
.cover-grid {
  /* 5.2vw puts the headline at ~3.7x the subline, close to the 3.1x the deck's
     own section headings run, so the cover reads as the loudest slide in the
     same family rather than a different one. */
  --hero-type: clamp(34px, 4.9vw, 80px);
  --hero-measure: min(100%, calc(9.87 * var(--hero-type)));
  /* The figure runs wider than the words and centres on them, so the pile
     reads as spreading past the headline instead of being boxed by it. Still
     derived from the same measure, so the overhang stays even at any width. */
  --fig-measure: min(100%, calc(1.22 * var(--hero-measure)));
  position: relative;
  z-index: 3;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: clamp(34px, 6vh, 66px);
  min-height: calc(100svh - 2 * var(--slide-y) - var(--slide-footer) - 38px);
  /* Heavier at the foot: centring on the true middle sits a beat low to the
     eye, so the extra bottom padding lifts the block back onto it. */
  padding: clamp(28px, 4.5vh, 54px) 0 clamp(46px, 8vh, 104px);
  text-align: center;
}

.cover-copy { max-width: min(1180px, 100%); }
/* Two lines, and they are the reason the text column is 1fr to the figure's
   0.37fr. At Space Grotesk 700 / -0.05em these two measure 9.87x and 9.86x
   their font size: an even rag, no ladder, no orphan. That balance is worth
   more than the ~7% of extra size a three-line break would buy, so the size
   comes out of the layout instead: full width, so nothing caps the type but
   the slide itself. */
.cover h1 {
  margin: 0;
  font-size: var(--hero-type);
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 0.9;
}
.cover h1 span { display: block; white-space: nowrap; }
.cover-copy > p {
  /* The subline shares the headline's measure outright, and the wrap decides
     the rag. Raising the max-width alone did nothing: `text-wrap: balance`
     minimises the longest line, so it held the text at 520/502/572 (0.87 of
     the headline) however wide the box got. `pretty` lets the lines run and
     only guards the last one, giving 599/577/417 at 1280 — 0.91 of the
     headline, and a taper rather than a heavy final line. */
  max-width: var(--hero-measure);
  margin: clamp(20px, 2.6vh, 30px) auto 0;
  color: var(--n-700);
  /* Tied to the headline rather than to the viewport. The subline's rendered
     width is a function of its font size, so a free-floating clamp would
     close the gap between the two blocks every time the headline came down.
     0.27 is the ratio that holds the subline at ~0.91 of the headline. */
  font-size: max(15px, calc(0.27 * var(--hero-type)));
  line-height: 1.55;
  /* `pretty`, not `balance`: it keeps the guard against a stranded last line
     (this sentence otherwise lands on "in production." alone at some widths)
     without also shortening the lines above it. */
  text-wrap: pretty;
}

/* Problem: the pipeline every team rebuilds, as one card with four stages.
   The single focal object on the slide: the flex column lets it claim the
   whole space under the heading instead of leaving a void at the bottom. */
.problem-slide { display: flex; flex-direction: column; }
.problem-slide .slide-heading { flex: none; }
/* The headline sets ~1025px wide, so the lede gets a measure to match it
   rather than the narrow 850px default, and the heading block is released
   from its 1120px cap to allow it. */
.problem-slide .slide-heading { max-width: none; }
/* Derived from the heading's type so the pair scales together. This block is
   left-aligned, so it wants full lines and a short last one, not the even rag
   the centred cover subline needs: 19x fills the first two and lets the third
   fall away, and runs ~1.25x the title's ink width. */
.slide-heading > p.problem-lede {
  max-width: min(100%, calc(19 * var(--section-type)));
}
/* The heading and card travel together as one centred block: leftover height
   splits above and below the pair instead of pooling at the slide's foot. */
.problem-slide .slide-heading { margin-top: auto; }
.problem-chain {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  flex: none;
  margin: clamp(28px, 3.8vh, 48px) 0 auto;
}
.problem-chain article {
  position: relative;
  display: grid;
  grid-template-rows: auto auto 1fr auto;
  align-items: start;
  gap: 16px;
  min-height: clamp(345px, 45vh, 465px);
  padding: clamp(28px, 2.7vw, 42px);
  border-left: 1px solid var(--n-150);
}
.problem-chain article:first-child { border-left: 0; }
.problem-chain article + article::after {
  content: "→";
  position: absolute;
  top: 50%;
  left: -10px;
  display: grid;
  width: 20px;
  height: 20px;
  place-items: center;
  color: var(--n-500);
  background: var(--surface-card);
  font: 13px/1 var(--font-mono);
  transform: translateY(-50%);
}
.problem-chain h3 { margin: 0; font-size: clamp(21px, 1.8vw, 29px); line-height: 1.05; letter-spacing: -0.025em; }
.problem-chain p { margin: 0; color: var(--n-700); font-size: clamp(14px, 1.15vw, 18px); line-height: 1.55; }
.problem-chain article > :last-child { margin-bottom: 0; }
/* Each stage closes on a small illustration that carries the detail the
   copy no longer spells out. */
/* Each illustration fills the card's full width and occupies the same fixed
   block, so all four align on the same top and bottom edges. */
.stage-art {
  display: grid;
  grid-template-rows: repeat(2, 37px);
  align-self: end;
  gap: 10px;
  width: 100%;
}
.art-row { display: flex; align-items: center; gap: 7px; }
.art-row > * { flex: 1; }

/* 01 — the same measurement arriving as six file types, piled up askew. */
/* Four to a row, filling the card width, each tile knocked askew. */
.art-scatter {
  grid-template-rows: repeat(2, 34px);
  grid-template-columns: repeat(4, 1fr);
  align-content: center;
  gap: 12px 7px;
  height: 84px;
}
.fmt {
  display: grid;
  place-items: center;
  height: 34px;
  color: #fff;
  border-radius: 7px;
  font: 700 9.5px/1 var(--font-mono);
  font-style: normal;
  box-shadow: var(--shadow-raised);
}
.fmt.cobalt { background: var(--cobalt); }
.fmt.orange { background: var(--orange); }
.fmt.yellow { color: var(--ink); background: var(--yellow); }
.fmt.ink { background: var(--ink); }
.art-scatter .fmt:nth-child(1) { transform: rotate(-5deg); }
.art-scatter .fmt:nth-child(2) { transform: rotate(3deg) translateY(-3px); }
.art-scatter .fmt:nth-child(3) { transform: rotate(-2deg) translateY(2px); }
.art-scatter .fmt:nth-child(4) { transform: rotate(5deg) translateY(-1px); }
.art-scatter .fmt:nth-child(5) { transform: rotate(-6deg) translateY(2px); }
.art-scatter .fmt:nth-child(6) { transform: rotate(2deg) translateY(-2px); }
.art-scatter .fmt:nth-child(7) { transform: rotate(-3deg) translateY(3px); }
.art-scatter .fmt:nth-child(8) { transform: rotate(4deg) translateY(-1px); }

/* 02 — field names from different sources that refuse to match. */
.art-mismatch { align-content: center; }
.mm-row { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 6px; }
.field {
  display: grid;
  place-items: center;
  height: 30px;
  padding: 0 6px;
  color: var(--n-600);
  background: var(--n-100);
  border: 1px solid var(--n-200);
  border-radius: 6px;
  font: 600 9.5px/1 var(--font-mono);
  font-style: normal;
}
.mm-x { color: var(--orange); font: 600 13px/1 var(--font-display); }

/* 04 — the live endpoint, serving data that stopped being current. */
.endpoint-bar {
  display: flex;
  height: 30px;
  align-items: center;
  gap: 7px;
  padding: 0 10px;
  color: var(--dark-text);
  background: var(--dark-panel);
  border-radius: 7px;
  font: 600 10px/1 var(--font-mono);
  font-style: normal;
}
.endpoint-bar em { margin-left: auto; color: var(--orange); font-style: normal; }
.stale-pip {
  width: 6px;
  height: 6px;
  background: var(--orange);
  border-radius: 50%;
  animation: live-pulse 2s var(--ease-standard) infinite;
}
.uptime-bars { display: flex; align-items: flex-end; gap: 4px; height: 22px; }
.uptime-bars i { flex: 1; height: 100%; background: var(--n-200); border-radius: 2px; }
.uptime-bars i.down { height: 38%; background: var(--orange); }
.takeaway {
  position: absolute;
  right: var(--slide-x);
  /* Cleared past the footer band: absolute positioning ignores the slide's
     reserved bottom padding. */
  bottom: calc(var(--slide-y) + var(--slide-footer));
  left: var(--slide-x);
  margin: 0;
  padding-top: 18px;
  border-top: 1px solid var(--n-250);
  font-size: clamp(19px, 1.6vw, 26px);
  font-weight: 600;
}

/* Solution: one slide per problem. Copy left, one focused visual right,
   echoing the cover's split so the section reads as a set. */
.solution-slide { display: flex; flex-direction: column; }
.solution-layout {
  display: grid;
  grid-template-columns: minmax(0, 0.86fr) minmax(430px, 1.14fr);
  align-items: center;
  gap: clamp(48px, 5vw, 88px);
  margin: auto 0;
  padding: clamp(24px, 4vh, 56px) 0;
}
.solution-copy { max-width: 620px; }
.solution-copy h2 {
  margin: 0;
  font-size: var(--section-type);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1;
}
/* Slide 08 splits its headline into two sentences; each gets its own line so
   the break lands on the full stop rather than wherever the column runs out. */
.solution-copy h2 > span { display: block; }
.solution-copy > p {
  max-width: 560px;
  margin: 22px 0 0;
  color: var(--n-700);
  font-size: clamp(16px, 1.3vw, 20px);
  line-height: 1.55;
}

/* 04 — what happens after the code is written. Prose buried the API at the end
   of six lines; as two labelled rows each is readable on its own. The eyebrow
   takes the brand cobalt rather than the usual quiet grey: at this size it is
   the only thing marking where one step ends and the next begins. */
.build-steps { display: grid; gap: clamp(11px, 1.4vh, 17px); margin: clamp(18px, 2.4vh, 28px) 0 0; max-width: 560px; }
.build-steps > div {
  display: grid;
  gap: clamp(4px, 0.6vh, 7px);
  padding-top: clamp(10px, 1.3vh, 15px);
  border-top: 1px solid var(--n-200);
}
.build-steps dt {
  color: var(--ink);
  font: 600 clamp(9.5px, 0.75vw, 12px)/1.2 var(--font-mono);
  letter-spacing: 0.15em;
  text-transform: uppercase;
}
.build-steps dd {
  margin: 0;
  color: var(--n-700);
  font-size: clamp(14px, 1.15vw, 18px);
  line-height: 1.5;
}

/* Solution visuals: the cover's flow, split so each slide owns one stage.
   Light cards against the dark hero panel. */
/* Same treatment as the cover card and the design system's Card: white on a
   hairline, soft radius, no resting shadow. */
.stage-card {
  display: grid;
  justify-items: center;
  gap: 20px;
  padding: clamp(26px, 2.6vw, 40px);
  background: var(--surface-card);
  border: 1px solid var(--n-200);
  border-radius: var(--radius-lg);
}

/* 03 — the shortlist the agent came back with for one question: who publishes
   it, in what format, and which ones survived the checks. Rejected rows keep
   their reason and drop out of colour, so the outcome needs no tick or cross. */
/* Narrower than the other two cards: its rows are a chip, a format and a
   short reason, so the full column width just opens a gap down the middle. */
.source-scan {
  justify-items: stretch;
  gap: clamp(13px, 1.6vh, 20px);
  width: 100%;
  /* Sized to the widest row rather than the column: publisher chip (110px at
     1280), format (35px), and the deciding fact (85px), plus gutters, padding
     and a little air. The full column width opened a hole down the middle. */
  max-width: min(100%, 412px);
  margin-inline: auto;
}
/* The ask that produced this shortlist. It rotates, so it reserves two lines
   and the card never changes height mid-swap. */
.scan-question {
  min-height: 2.4em;
  margin: 0;
  padding-bottom: clamp(11px, 1.4vh, 16px);
  border-bottom: 1px solid var(--n-150);
  font-size: clamp(15px, 1.28vw, 20px);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.2;
}
.scan-list { display: grid; gap: clamp(9px, 1.2vh, 14px); margin: 0; padding: 0; list-style: none; }
/* Fixed columns, not auto: rows are separate grids so they can fade one at a
   time, and auto columns would put each row's format at a different x. */
.scan-row {
  display: grid;
  grid-template-columns: clamp(88px, 8.8vw, 122px) clamp(28px, 2.8vw, 39px) minmax(0, 1fr);
  align-items: center;
  gap: clamp(9px, 1vw, 14px);
}
.scan-row > .scan-src { justify-self: start; }
.scan-src {
  padding: clamp(6px, 0.55vw, 9px) clamp(10px, 0.9vw, 14px);
  border-radius: 8px;
  color: var(--n-500);
  background: var(--n-150);
  font: 500 clamp(9.5px, 0.8vw, 12px)/1 var(--font-mono);
  font-style: normal;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
}
.scan-src.cobalt { color: #fff; background: var(--cobalt); }
.scan-src.orange { color: #fff; background: var(--orange); }
.scan-src.yellow { color: var(--ink); background: var(--yellow); }
.scan-src.ink { color: var(--paper); background: var(--ink); }
.scan-fmt { color: var(--n-400); font: 500 clamp(9px, 0.75vw, 11.5px)/1 var(--font-mono); }
/* The deciding fact, on every row: the cadence or grain that qualified the
   source, or the thing that disqualified it. Lower case, because these carry
   units and arrows that uppercase mangles. */
.scan-why {
  justify-self: end;
  color: var(--n-400);
  font: 500 clamp(9px, 0.75vw, 11.5px)/1 var(--font-mono);
  letter-spacing: 0.03em;
  white-space: nowrap;
}
.scan-row.is-out .scan-fmt { opacity: 0.6; }
/* Each part fades on its own offset, so the card never empties at once. */
.scan-question,
.scan-row { transition: opacity 0.22s var(--ease-standard); }
.scan-question.is-swapping,
.scan-row.is-swapping { opacity: 0; }

/* 04 — the joined table, the whole point of the slide. Taller than the other
   cards because a dataset has to look like a dataset, not a sample of one. */
.build-card { justify-items: stretch; gap: clamp(14px, 1.8vh, 22px); width: 100%; }
.build-table {
  width: 100%;
  border-collapse: collapse;
  font: 500 clamp(8.5px, 0.72vw, 11px)/1 var(--font-mono);
  table-layout: auto;
  text-align: left;
}
.build-table th {
  padding: 0 0 10px;
  color: var(--n-500);
  font-weight: 500;
  letter-spacing: 0.06em;
  white-space: nowrap;
  border-bottom: 1px solid var(--n-200);
}
.build-table td {
  padding: clamp(8px, 1.15vh, 13px) 0;
  color: var(--ink);
  white-space: nowrap;
  border-bottom: 1px solid var(--n-150);
}
.build-table tr > *:last-child { text-align: right; }
/* The source column is where the brand colour earns its place: one tone per
   publisher, held constant across every dataset, so the colours show the join
   crossing sources rather than decorating the table. */
.src-tag {
  display: inline-block;
  padding: 4px clamp(6px, 0.6vw, 9px);
  border-radius: 5px;
  color: var(--paper);
  background: var(--ink);
  font-style: normal;
  letter-spacing: 0.04em;
}
.src-tag.cobalt { color: #fff; background: var(--cobalt); }
.src-tag.orange { color: #fff; background: var(--orange); }
.src-tag.yellow { color: var(--ink); background: var(--yellow); }
.src-tag.ink { color: var(--paper); background: var(--ink); }
/* Rows land as bars, then resolve to values: the table filling in. */
.build-table tbody tr.is-filling td { color: transparent; }
.build-table tbody tr.is-filling td:not(:last-child) {
  background:
    linear-gradient(var(--n-150), var(--n-150)) left center / 74% 9px no-repeat;
}
.build-table tbody tr.is-filling .src-tag { color: transparent; opacity: 0.35; }
.build-table tbody tr:last-child td { border-bottom: 0; }
/* The caption, the header and each row fade on their own offset, the same wave
   as the cover and 03, so the card never blanks mid-rotation. Rows also slide
   up a touch as they land, which reads as the table filling in. */
.build-card .mono-label,
.build-table thead tr,
.build-table tbody tr {
  transition: opacity 0.2s var(--ease-standard), transform 0.28s var(--ease-standard);
}
.build-card .is-swapping { opacity: 0; transform: translateY(5px); }

/* 05 — the demo's own path: the project's datasets, then one opens and its
   notebook assembles. The same bar and the same view-follows-the-agent
   behaviour. Content is read from the running demo rather than composed. */
/* justify-items: stretch overrides .stage-card's centring, which otherwise
   shrinks the pane to its content instead of filling the card. */
.app-frame { display: grid; justify-items: stretch; gap: 0; width: 100%; padding: 0; overflow: hidden; }
.app-mark { flex: none; width: clamp(15px, 1.4vw, 20px); height: auto; }

/* The list and the notebook share one grid cell so opening a dataset swaps
   the pane in place, with no height jump between the two states. */
.app-main { display: grid; grid-template-rows: auto minmax(0, 1fr); min-width: 0; }
.app-list, .nbk-view { grid-area: 2 / 1; }
.app-bar {
  display: flex;
  align-items: center;
  gap: clamp(8px, 1vw, 14px);
  padding: clamp(9px, 1vw, 13px) clamp(11px, 1.2vw, 16px);
  border-bottom: 1px solid var(--n-200);
}
.app-project { flex: 1 1 auto; min-width: 0; display: grid; gap: 1px; }
.app-project small { color: var(--n-500); font: 500 clamp(6px, 0.55vw, 8.5px)/1 var(--font-mono); }
.app-project b {
  overflow: hidden;
  font-size: clamp(8px, 0.72vw, 11px);
  font-weight: 700;
  letter-spacing: -0.01em;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.app-project b::after { content: " \25be"; color: var(--n-400); }
.app-tab {
  flex: none;
  padding: clamp(4px, 0.5vw, 7px) clamp(6px, 0.7vw, 10px);
  border: 1px solid var(--n-200);
  border-radius: 6px;
  background: var(--n-50);
  font: 500 clamp(6.5px, 0.58vw, 9px)/1 var(--font-mono);
  white-space: nowrap;
}
.app-tab i { font-style: normal; color: var(--n-400); }

/* The project screen, on the demo's own dataset table: symbol, name, version,
   shape and visibility. Rows land one after another, then the one being opened
   takes the cobalt rail and the whole table clears out of the way. */
.app-list {
  --app-cols: minmax(0, 1fr) 34px clamp(88px, 9.5vw, 132px) clamp(48px, 5vw, 66px);
  display: grid;
  align-content: start;
  /* Ten of the project's thirteen datasets fit the pane at 720px. On a shorter
     viewport the tail clips rather than pushing the card past the slide. */
  overflow: hidden;
  transition: opacity 0.3s var(--ease-standard), transform 0.4s var(--ease-standard);
}
.app-list.is-gone { opacity: 0; transform: translateY(-8px); pointer-events: none; }
.app-head, .app-item {
  display: grid;
  grid-template-columns: var(--app-cols);
  align-items: center;
  gap: clamp(8px, 1vw, 14px);
  padding: 0 clamp(11px, 1.2vw, 16px);
}
.app-head {
  padding-bottom: clamp(6px, 0.8vh, 9px);
  padding-top: clamp(8px, 1vh, 12px);
  border-bottom: 1px solid var(--n-200);
  color: var(--n-500);
  font: 600 clamp(6px, 0.53vw, 8px)/1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.app-rows { display: contents; margin: 0; padding: 0; list-style: none; }
.app-item {
  padding-block: clamp(8px, 1.15vh, 13px);
  border-bottom: 1px solid var(--n-150);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.3s var(--ease-standard), transform 0.3s var(--ease-standard),
    background-color 0.25s var(--ease-standard), box-shadow 0.25s var(--ease-standard);
}
.app-item.is-in { opacity: 1; transform: none; }
.app-name { display: flex; align-items: center; gap: clamp(7px, 0.8vw, 11px); min-width: 0; }
.app-name > span { display: grid; gap: 2px; min-width: 0; }
/* Row symbols are the same tiles as the cover card, at list scale. */
.app-name .gtile { flex: none; width: clamp(17px, 1.7vw, 24px); height: clamp(17px, 1.7vw, 24px); border-radius: 5px; }
.app-item b {
  overflow: hidden;
  font-size: clamp(8px, 0.72vw, 11.5px);
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.app-item small {
  overflow: hidden;
  color: var(--n-500);
  font: 500 clamp(6px, 0.53vw, 8.5px)/1 var(--font-mono);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.app-cell {
  overflow: hidden;
  color: var(--n-600);
  font: 500 clamp(6.5px, 0.56vw, 9px)/1 var(--font-mono);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.app-item.is-picked {
  background: color-mix(in srgb, var(--cobalt) 6%, #fff);
  box-shadow: inset 2px 0 0 var(--cobalt);
}
.app-item.is-picked .app-cell { color: var(--cobalt); }

/* The view follows the newest block, which is what the demo does while an
   agent is writing. The track is translated rather than scrolled so the
   movement eases with the rest of the deck. */
.nbk-view {
  /* Tall enough to hold several blocks at once, so the follow reads as a view
     keeping up rather than a letterbox. Takes the slack the sidebar used to
     occupy: at 720px the card lands ~500px in a 530px column. */
  height: clamp(200px, 62vh, 460px);
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.35s var(--ease-standard);
}
.nbk-view.is-in { opacity: 1; }
.nbk-track {
  display: grid;
  gap: clamp(11px, 1.5vh, 18px);
  padding: clamp(14px, 1.7vw, 24px) clamp(14px, 1.7vw, 24px) clamp(20px, 2.4vw, 32px);
  transition: transform 0.5s var(--ease-standard);
}
.nbk-track > * {
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.3s var(--ease-standard), transform 0.3s var(--ease-standard);
}
.nbk-track > .is-in { opacity: 1; transform: none; }
/* A hairline opens each new section, the way the notebook separates them. */
.nbk-rule {
  margin-top: clamp(6px, 0.9vh, 12px);
  padding-top: clamp(15px, 1.9vh, 24px);
  border-top: 1px solid var(--n-150);
}

.nbk-md {
  margin: 0;
  font-size: clamp(9.5px, 0.84vw, 13px);
  font-weight: 700;
  letter-spacing: -0.01em;
}
/* Cells run the full width now that the execution counter is gone. */
.nbk-code {
  margin: 0;
  padding: clamp(10px, 1.2vw, 16px) clamp(12px, 1.4vw, 18px);
  border-radius: var(--radius-sm);
  background: var(--ink);
  overflow: hidden;
}
.nbk-code code {
  display: block;
  min-height: 1.5em;
  color: var(--dark-text);
  font: 500 clamp(7.5px, 0.66vw, 10.5px)/1.55 var(--font-mono);
  white-space: pre-wrap;
  word-break: break-word;
}
/* Token colours are the demo's notebook-cell palette from canvas.css, which
   differs from the canvas palette the --syn-* tokens carry. */
.nbk-code .tok-keyword { color: #8cadff; }
.nbk-code .tok-string { color: #ffc613; }
.nbk-code .tok-number { color: #f08a61; }
.nbk-code .tok-function { color: #e8dd8a; }
.nbk-code .tok-comment { color: #8f8a7e; }

/* The question output the first cell prints. */
.nbk-kv { display: grid; gap: clamp(5px, 0.7vh, 9px); margin: 0; }
.nbk-kv div { display: grid; grid-template-columns: clamp(74px, 8vw, 116px) minmax(0, 1fr); gap: clamp(9px, 1.1vw, 16px); }
.nbk-kv dt { color: var(--n-500); font: 500 clamp(7px, 0.6vw, 9.5px)/1.4 var(--font-mono); }
.nbk-kv dd { margin: 0; font: 500 clamp(7px, 0.6vw, 9.5px)/1.4 var(--font-mono); }

/* The column inspector the cell emits, as the demo renders it. */
.nbk-out {
  display: grid;
  gap: clamp(9px, 1.2vh, 15px);
  margin: 0;
  padding: clamp(11px, 1.3vw, 17px);
  border: 1px solid var(--n-200);
  border-radius: var(--radius-sm);
}
.nbk-out figcaption { display: flex; align-items: baseline; gap: clamp(6px, 0.8vw, 11px); }
.nbk-eyebrow {
  color: var(--n-400);
  font: 600 clamp(6.5px, 0.54vw, 8.5px)/1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.nbk-out figcaption b { font: 500 clamp(8.5px, 0.74vw, 11.5px)/1 var(--font-mono); }
.nbk-out figcaption small { color: var(--n-500); font: 500 clamp(7px, 0.58vw, 9px)/1 var(--font-mono); }
.nbk-stats { display: flex; gap: clamp(14px, 1.8vw, 30px); margin: 0; }
.nbk-stats div { display: grid; gap: 2px; }
.nbk-stats dt {
  color: var(--n-400);
  font: 600 clamp(6.5px, 0.54vw, 8.5px)/1 var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.nbk-stats dd { margin: 0; font: 500 clamp(8px, 0.68vw, 11px)/1 var(--font-mono); }
/* Four profiled values, one per bin, so the bars are level by construction. */
.nbk-bars {
  display: flex;
  align-items: flex-end;
  gap: clamp(5px, 0.7vw, 11px);
  height: clamp(40px, 5.4vh, 68px);
  margin-top: clamp(6px, 0.8vh, 10px);
  /* No padding here: a gap between the bars and the rule reads as a 1px
     blank line under the chart rather than as a baseline. */
  border-bottom: 1px solid var(--n-200);
}
.nbk-bars i {
  position: relative;
  flex: 1 1 0;
  height: 0;
  border-radius: 2px 2px 0 0;
  background: var(--cobalt);
  transition: height 0.4s var(--ease-standard);
}
.nbk-bars i.is-in { height: var(--h); }
/* The demo labels every bar with its count. */
.nbk-bars i::before {
  content: attr(data-count);
  position: absolute;
  top: -1.4em;
  left: 0;
  width: 100%;
  color: var(--n-400);
  font: 500 clamp(6.5px, 0.54vw, 8.5px)/1 var(--font-mono);
  text-align: center;
  opacity: 0;
  transition: opacity 0.3s var(--ease-standard) 0.2s;
}
.nbk-bars i.is-in::before { opacity: 1; }
.nbk-axis { display: flex; gap: clamp(5px, 0.7vw, 11px); }
.nbk-axis i {
  flex: 1 1 0;
  color: var(--n-400);
  font: 500 clamp(6.5px, 0.54vw, 8.5px)/1 var(--font-mono);
  font-style: normal;
  text-align: center;
}

/* 08 — the demo's own Discover screen, trimmed to three columns. The publisher
   column is the argument: Mostly Right sits in a list next to Open Energy,
   Open Economics and Climate Commons, which is the platform claim made by the
   UI rather than by a sentence. Data is the mockup's publicDatasets fixture. */
.discover-card { justify-items: stretch; gap: 0; width: 100%; padding: 0; overflow: hidden; }
.dsc-body { display: grid; gap: clamp(11px, 1.5vh, 16px); padding: clamp(14px, 1.7vw, 22px); }
.dsc-search {
  margin: 0;
  padding: clamp(9px, 1.2vh, 13px) clamp(12px, 1.3vw, 16px);
  border: 1px solid var(--n-200);
  border-radius: var(--radius-pill);
  color: var(--n-400);
  background: var(--n-50);
  font: 500 clamp(9.5px, 0.78vw, 12px)/1 var(--font-mono);
}
.dsc-topics { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; }
.dsc-topics i {
  padding: clamp(5px, 0.6vw, 7px) clamp(8px, 0.85vw, 12px);
  border: 1px solid var(--n-200);
  border-radius: var(--radius-pill);
  color: var(--n-500);
  font: 500 clamp(8.5px, 0.7vw, 11px)/1 var(--font-mono);
  font-style: normal;
}
.dsc-topics i.is-on { color: #fff; background: var(--ink); border-color: var(--ink); }

.dsc-head, .dsc-list li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(88px, 9.5vw, 132px) clamp(52px, 5.4vw, 74px);
  align-items: center;
  gap: clamp(8px, 1vw, 14px);
}
.dsc-head {
  padding-bottom: clamp(6px, 0.8vh, 9px);
  border-bottom: 1px solid var(--n-200);
  color: var(--n-500);
  font: 600 clamp(6px, 0.53vw, 8px)/1 var(--font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.dsc-list { display: grid; margin: 0; padding: 0; list-style: none; }
.dsc-list li { padding: clamp(8px, 1.1vh, 12px) 0; border-bottom: 1px solid var(--n-150); }
.dsc-list li:last-child { border-bottom: 0; }
.dsc-name {
  display: flex;
  align-items: center;
  gap: clamp(7px, 0.8vw, 11px);
  min-width: 0;
  overflow: hidden;
  font-size: clamp(8.5px, 0.74vw, 11.5px);
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.dsc-name .gtile { flex: none; width: clamp(17px, 1.7vw, 24px); height: clamp(17px, 1.7vw, 24px); border-radius: 5px; }
.dsc-pub, .dsc-dl {
  overflow: hidden;
  color: var(--n-600);
  font: 500 clamp(7.5px, 0.62vw, 10px)/1.2 var(--font-mono);
  white-space: nowrap;
  text-overflow: ellipsis;
}
.dsc-dl { color: var(--n-400); }

/* 07 — two numbers, kept apart: what teams spend on engineers wrangling public
   sources, and what they spend with vendors when those sources run out. */
/* One card split by a hairline, the same construction as 02's problem chain,
   so the two slides read as the same object rather than as a chain and a
   pair of tiles. */
/* Same trick as the problem slide: heading and card travel as one centred
   block, so shrinking the card splits the freed height above and below the
   pair instead of pooling it at the slide's foot. */
.tam-slide { display: flex; flex-direction: column; }
.tam-slide .slide-heading { flex: none; margin-top: auto; }
.tam-split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  flex: none;
  margin: clamp(30px, 4.2vh, 52px) 0 auto;
  /* Enough to keep the two halves from collapsing onto their content, but
     well under the old 50vh, which padded each half by 85px of nothing. */
  min-height: clamp(200px, 32vh, 300px);
}
.tam-half {
  display: grid;
  align-content: center;
  gap: clamp(12px, 1.5vh, 18px);
  /* Block padding is set apart from inline: the halves are wide and short, so
     the sides want the old measure and the top and bottom do not. */
  padding: clamp(20px, 2.4vh, 30px) clamp(28px, 2.7vw, 42px);
  border-left: 1px solid var(--n-150);
}
.tam-half:first-child { border-left: 0; }
.tam-half strong {
  font-size: clamp(46px, 5.4vw, 88px);
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 0.95;
}
.tam-half p { margin: 0; color: var(--n-700); font-size: clamp(13px, 1.05vw, 16px); line-height: 1.5; }
/* Where the number came from, small enough to be a footnote and present
   enough that nobody has to ask. */
.tam-src {
  color: var(--n-400);
  font: 500 clamp(9px, 0.74vw, 11px)/1.4 var(--font-mono);
  letter-spacing: 0.03em;
}



/* 06 — the harness as a wall of datasets it can carry. Copy left, panel right,
   the same frame the solution slides use. The grid keeps turning over: shapes
   swap on a wave so the panel never blanks, and a handful carry brand colour
   so the field has a pulse rather than being uniform grey.
   Tried and rejected on the way here: two cards side by side (the halves never
   balance), a converging rail (spokes read as stray marks), an overlapping
   deck for the source slot (collapses into a smudge). */
/* Smaller than the other stage cards on purpose: this one is an illustration,
   not a screen, and at full column width the tiles read as a data table. */
.harness-card {
  justify-items: stretch;
  gap: clamp(14px, 1.8vh, 22px);
  width: 100%;
  max-width: min(100%, 396px);
  margin-inline: auto;
}
.hx-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(8px, 0.9vw, 13px);
}
.hx-tile {
  position: relative;
  aspect-ratio: 1;
  border: 1px solid var(--n-200);
  border-radius: var(--radius-sm);
  background: var(--n-50);
  transition: opacity 0.4s var(--ease-standard), background-color 0.4s var(--ease-standard);
}
/* One shape per tile: in this brand a mark is a dataset, so a field of marks
   that never repeats is a field of datasets that never repeats. */
.hx-tile::after {
  content: "";
  position: absolute;
  inset: 21%;
  background: var(--hx-ink, var(--n-300));
  -webkit-mask-image: var(--shape);
  mask-image: var(--shape);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
.hx-tile.on-cobalt { background: var(--cobalt); border-color: var(--cobalt); --hx-ink: var(--n-100); }
.hx-tile.on-yellow { background: var(--yellow); border-color: var(--yellow); --hx-ink: var(--ink); }
.hx-tile.on-orange { background: var(--orange); border-color: var(--orange); --hx-ink: var(--ink); }
.hx-tile.on-ink { background: var(--ink); border-color: var(--ink); --hx-ink: var(--n-100); }
.hx-tile.is-swapping { opacity: 0; }

/* Team */
.team-heading { max-width: 1130px; }
.team-heading > p { max-width: 1040px; }
.founder-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(18px, 2vw, 28px); margin-top: clamp(28px, 4.5vh, 52px); }
.founder-card { display: flex; min-height: clamp(230px, 30vh, 310px); flex-direction: column; padding: clamp(24px, 2.25vw, 36px); }
.founder-head { display: flex; align-items: center; gap: 17px; }
.founder-head img { width: 74px; height: 74px; border-radius: var(--radius-menu); object-fit: cover; filter: grayscale(1); }
.founder-head h3 { margin: 0; font-size: clamp(22px, 1.8vw, 29px); }
.founder-head span { display: block; margin-top: 7px; color: var(--cobalt); }
.founder-card p { margin: 28px 0 0; color: var(--n-700); font-size: clamp(14px, 1.08vw, 17px); line-height: 1.55; }
/* Who backed us, at the foot of the slide: the fund names are the signal, so
   they sit on one quiet line under the cards rather than inside one of them.
   Both lockups are placed as each fund draws them. Hustle Fund's four arrows
   are purple, red, orange and yellow; 2080's own mark is black, so it is the
   one thing here that is already close to this deck's ink. */
.team-backers {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(20px, 2.4vw, 34px);
  margin: clamp(34px, 5vh, 58px) 0 0;
}
/* Sized by eye, not by height: the Hustle Fund lockup is one line and 8:1,
   the 2080 lockup stacks two, so equal heights would read badly unbalanced. */
.backer { display: block; width: auto; }
.backer--hustle { height: 21px; }
.backer--2080 { height: 32px; }

/* Controls */
.progress-rail { position: fixed; z-index: 50; right: 0; bottom: 0; left: 0; height: 3px; background: rgba(23,22,15,.12); }
.progress-rail span { display: block; width: 11.111%; height: 100%; background: var(--cobalt); transition: width var(--dur-considered) var(--ease-standard); }
.deck-controls {
  position: fixed;
  z-index: 60;
  right: 18px;
  bottom: 16px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px;
  border: 1px solid rgba(216,210,196,.78);
  border-radius: var(--radius-menu);
  background: rgba(250,248,242,.88);
  box-shadow: var(--shadow-raised);
  backdrop-filter: blur(10px);
}
.deck-controls > span { padding: 0 8px; color: var(--n-600); font: 10px/1 var(--font-mono); }
.deck-controls button { display: grid; width: 32px; height: 32px; place-items: center; border: 0; border-radius: var(--radius-sm); color: var(--ink); background: transparent; cursor: pointer; }
.deck-controls button:hover { background: var(--n-100); }
.deck-controls button:disabled { opacity: .3; cursor: default; }

@media (max-width: 1100px) {
  .cover-copy { max-width: 830px; }
  .problem-chain article { min-height: clamp(300px, 40vh, 400px); }
  /* Only the chip size changes here. Count and position are per dataset now
     and set from deck.js, and the percentages hold at any field width. */
  .fig-chip { padding: 8px 13px; border-radius: 8px; font-size: 10px; }
}

@media (max-width: 760px) {
  :root { --slide-x: 22px; --slide-y: 22px; }
  .slide { min-height: 100svh; overflow: visible; padding-bottom: 56px; }
  .hx-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  /* The pattern field bleeds past the slide edge by design; keep it clipped
     so it never paints over the divider of the next slide. */
  .brand-mark svg { width: 132px; }
  .slide-heading h2, .team-heading h2 { font-size: clamp(38px, 11vw, 52px); line-height: 1; }
  .slide-heading h2 > span { display: inline; }
  .slide-heading h2 > span + span::before { content: " "; }
  .cover-grid { min-height: auto; padding-top: 30px; gap: 26px; }
  /* A stack: no phone is wide enough to set the pile beside the card. */
  .cover-figure { flex-direction: column; align-items: center; width: 100%; height: auto; gap: 8px; }
  .fig-mess { flex: none; width: 100%; height: 180px; }
  .fig-chip { padding: 9px 15px; font-size: 10.5px; }
  /* Column layout: the desktop nudge toward the card would just push it off
     the centre axis here. */
  .fig-arrow { width: 20px; height: auto; margin-right: 0; transform: rotate(90deg); }
  .fig-card { width: min(100%, 200px); height: auto; padding: 10px; gap: 9px; }
  /* The right column's minmax(420px, ...) floor starves the left one below
     this width: the copy collapses to a sliver and wraps a word per line. */
  /* The 05 bar loses its clock at this width so the project name keeps room. */
    .app-project b { font-size: 11px; }
  .app-project small { font-size: 8.5px; }
  /* minmax(0,...) not 1fr: the stage cards hold nowrap tables whose minimum
     content width would otherwise size the track wider than the slide and
     drag the copy column off the right edge. */
  .solution-layout { grid-template-columns: minmax(0, 1fr); gap: 30px; padding: 26px 0; }
  .solution-copy { max-width: none; }
  .solution-copy h2 { font-size: clamp(32px, 9vw, 44px); }
  .cover h1 { font-size: clamp(40px, 11.4vw, 56px); line-height: 1; text-wrap: balance; }
  .cover h1 span { display: inline; white-space: normal; }
  .cover h1 span + span::before { content: " "; }
  /* The desktop measure tracks the headline's text width; on one column it
     just starves the paragraph. */
  .cover-copy > p { max-width: none; font-size: 19px; }
  /* Restores the single-column stack that the old multi-selector rule carried
     before its other classes were deleted: the founder row is the last
     three-column grid left, and it overflowed 375px without this. */
  .founder-grid { grid-template-columns: 1fr; }
  .founder-card { min-height: 0; }
  .takeaway { position: static; margin-top: 30px; }
  /* Slide 05's dataset table keeps the name and drops the metric columns:
     four columns will not fit 375px without truncating every one of them. */
  .app-list { --app-cols: minmax(0, 1fr); }
  .app-head span:nth-child(n + 2), .app-item .app-cell { display: none; }
  /* Stacked cards: content-height rows, and the format pile drops its last
     two tiles so it still reads as a pile rather than a wrapped list. */
  .problem-slide, .tam-slide { display: block; }
  /* Their headings centre with auto margins on desktop, which collapse to
     zero once the slide is a block, leaving the headline jammed under the
     mark. 26px is what the solution slides sit at. */
  .problem-slide .slide-heading, .tam-slide .slide-heading { margin-top: 26px; }
  .problem-chain { grid-template-columns: 1fr; margin: 26px 0 0; }
  .problem-chain article {
    min-height: 0;
    border-left: 0 !important;
    border-top: 1px solid var(--n-150);
  }
  .problem-chain article:first-child { border-top: 0; }
  /* Stacked cards read top to bottom, so the between-column arrows go. */
  .problem-chain article + article::after { display: none; }
  /* Two 165px columns cramp both halves and strand the source lines, so the
     opportunity card stacks and the divider moves from side to top. */
  .tam-split { grid-template-columns: 1fr; min-height: 0; margin: 26px 0 0; }
  .tam-half {
    border-left: 0;
    border-top: 1px solid var(--n-150);
  }
  .tam-half:first-child { border-top: 0; }
  .stage-art { grid-template-rows: repeat(2, 34px); gap: 8px; }
  .art-scatter { height: 76px; }
  .art-scatter .fmt:nth-child(n + 7) { display: none; }
  .slide-heading > p.problem-lede { max-width: none; }
  /* Stacked slides grow past the viewport, so the footer rides the content
     instead of being pinned to a bottom that is no longer the screen's. The
     contact stays: on a phone this deck is a link someone was sent, and the
     address is the only way to answer it. */
  .slide-footer { position: static; margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--n-200); }
  .deck-controls { display: none; }
}

/* Below ~380px even the smallest mono size cannot hold five nowrap columns.
   Every rotation of this table puts a timestamp second and repeats the same
   value down the column, so that is the one to drop. */
@media (max-width: 380px) {
  .build-table th:nth-child(2),
  .build-table td:nth-child(2) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
      }

@page { size: 13.333in 7.5in; margin: 0; }

@media print {
  html, body { width: 13.333in; background: #fff; }
  .slide { width: 13.333in; min-height: 7.5in; height: 7.5in; break-after: page; page-break-after: always; }
  .deck-controls, .progress-rail, .skip-link { display: none !important; }
}
