/* BYTE COPY of m3-cfo/decks/assets/deck.css. Do not hand-edit.
   Re-extract:  cp ~/projects/m3-cfo/decks/assets/deck.css .
   Design standard: ~/projects/m3-cfo/decks/STYLE_GUIDE.md  */
/* ═══════════════════════════════════════════════════════════════════════════
   M3 pitch decks — design system.

   Measured from the source template (Green Modern Agriculture Pitch Deck
   Presentation.pptx), not eyeballed:

     canvas   20in x 11.25in  → 16:9
     ground   #12332A         deep forest
     accent   #B3E67D         lime
     text     #FFFFFF         white
     type     110pt hero · 60pt section · 28pt body · 19pt small
              on a 1440pt-wide canvas

   Those three hex values are the ONLY colors in the source file. Every panel
   tint in it comes from exactly two translucent fills at 80% alpha, lime and
   forest, layered over photography and the ground. 36 such shapes across the
   10 slides. Reproduce tints that way; do not introduce new greens.

   SCALING. Everything inside a slide is expressed in `--u`, one scale unit.
   On a real slide `--u` is 1cqw, i.e. 1% of the slide's own width, so the
   whole composition scales as one rigid object while the text stays real
   text: selectable, searchable, zoomable, printable. On a phone `--u` flips
   to a rem so the deck reflows to a readable document. One knob, one place.

   Type maps straight off the template: 110/1440 = 7.64u, 60 = 4.17u,
   28 = 1.94u, 19 = 1.32u.
   ═══════════════════════════════════════════════════════════════════════════ */

@font-face {
  font-family: "Glacial Indifference";
  src: url("./fonts/GlacialIndifference-Regular.woff2") format("woff2"),
       url("./fonts/GlacialIndifference-Regular.woff") format("woff");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Glacial Indifference";
  src: url("./fonts/GlacialIndifference-Bold.woff2") format("woff2"),
       url("./fonts/GlacialIndifference-Bold.woff") format("woff");
  font-weight: 700; font-style: normal; font-display: swap;
}

:root {
  /* ── THE DARK DECK IS THE DEFAULT, AND THAT IS DELIBERATE ─────────────────
     Light mode is opt-in through the toggle, never seeded from
     `prefers-color-scheme`. Every headless tool on this box runs with no
     localStorage and Chromium's default light preference: tools/export.mjs,
     tools/shoot.mjs and web-deck/verify.mjs. Following the media query would
     silently flip the shipped PDF, the PPTX, every screenshot and every clip
     check to light. Defaulting to dark means no existing artifact can move.

     Roles, not appearances. Everything below names what a colour DOES, so the
     one override block further down can swap the whole deck without any rule
     in between knowing there are two themes. */

  --ink: #12332A;                       /* the forest ground and, on light, the body text */
  --lime: #B3E67D;                      /* the template's accent, a FILL in both themes    */
  --paper: #FFFFFF;

  /* THE LIME CANNOT BE TEXT ON A LIGHT GROUND. Measured: 1.45:1 on white. Light
     mode carries type in M3 deep teal instead -- 7.86:1 on the page ground and
     8.65:1 on a white panel -- which is the same colour the financial models
     site uses for headings, so a reader moving between the two properties meets
     one accent doing one job. A deepened lime was built and compared first; it
     held the template's two-tone title but sat oddly beside the site.
  /* LITERALS, NOT `var(--lime)`. The capital donut sets fill="var(--accent)" as
     an SVG presentation attribute, and Chromium resolves a var() chain only one
     level deep there: `--accent` -> `var(--lime)` -> hex fell through to black,
     so the $40.0M in the middle of the donut was invisible on the dark panel.
     Measured at 1.81:1 before this. Keep both of these literal. */
  --accent: #B3E67D;                    /* accent TEXT: headings, eyebrows, stat numbers */
  --accent-fill: #B3E67D;               /* accent SURFACES: the accent panel, the skip link */
  --on-accent-fill: #12332A;            /* TYPE ON that fill: ink on lime, paper on teal    */
  --accent-on-dark: #B3E67D;            /* accent TEXT inside a wash, which is dark in both */

  --void: #0b1f19;                      /* behind the slides, and the nav and tip surface */
  --ground: var(--ink);                 /* the slide itself                               */
  --text: var(--paper);
  --frame: #000;                        /* the letterbox behind an embedded player        */

  /* The two washes. This is the whole panel system. */
  --wash-ink: rgba(18, 51, 42, .8);
  --wash-lime: rgba(179, 230, 125, .8);

  /* Flat panel, a touch above the ground so a block grid reads without lines. */
  --panel: #17402F;
  --panel-hi: #1D4C38;

  --rule: rgba(179, 230, 125, .35);
  --hair: rgba(255, 255, 255, .16);
  --track: rgba(255, 255, 255, .18);

  --font: "Glacial Indifference", "Avenir Next", Avenir, Helvetica, Arial, sans-serif;

  /* Flag accent for the "what is still open" slides. Light enough to clear
     text contrast on the forest ground; matches the site's dark-theme bad. */
  --flag: #f08a7e;

  /* Referenced by viz.css's chart focus ring and defined nowhere until now, so
     that outline has been rendering as nothing. Keyboard users get it back. */
  --m3-teal: #2aa1aa;
}

/* Light mode was retired on 2026-08-21. Every light-scoped rule is gone,
   not merely unreachable, so setting data-theme="light" by hand does
   nothing. This held the light token set.
   git show 8dbb875:decks/assets/deck.css has the removed block. */

* { box-sizing: border-box; }

html { background: var(--void); }
body {
  margin: 0;
  font-family: var(--font);
  background: var(--void);
  color: var(--text);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }

/* ── the deck ───────────────────────────────────────────────────────────── */

.deck { margin: 0 auto; }

.slide {
  --u: 1cqw;
  container-type: inline-size;
  position: relative;
  /* A 16:9 box at full window width is taller than the window on any screen
     wider than it is tall, so the slide ran off the bottom and the reader had
     to scroll to see a slide. Cap the width at whatever keeps that 16:9 box
     inside the viewport height and centre it; the ground either side is the
     page background, which is the same family of green. `dvh` follows a mobile
     URL bar, `vh` is the fallback for anything that does not know it. */
  width: min(100%, calc(100vh * 16 / 9));
  width: min(100%, calc(100dvh * 16 / 9));
  margin-inline: auto;
  aspect-ratio: 16 / 9;
  background: var(--ground);
  overflow: hidden;
  color: var(--text);
}

/* Older browsers with no container queries fall back to viewport units. A
   slide is full-bleed on desktop, so 1vw is very close to 1cqw there. */
@supports not (container-type: inline-size) {
  .slide { --u: 1vw; }
}

/* One slide visible at a time. `.deck--flow` (print, and phones) shows all. */
.deck:not(.deck--flow) .slide { display: none; }
.deck:not(.deck--flow) .slide.is-current { display: block; }

.slide__pad {
  position: relative;
  width: 100%; height: 100%;
  padding: 4.2u;
  padding: calc(4.2 * var(--u));
  display: flex;
  flex-direction: column;
  gap: calc(1.6 * var(--u));
}

/* ── type ───────────────────────────────────────────────────────────────── */

.hero {
  font-size: calc(7.64 * var(--u));
  line-height: 1.02;
  font-weight: 700;
  margin: 0;
  letter-spacing: -0.01em;
}
.title {
  font-size: calc(4.17 * var(--u));
  line-height: 1.08;
  font-weight: 400;
  margin: 0;
  letter-spacing: -0.005em;
  /* Every other title fits one line, where this is a no-op. The one that does not
     otherwise breaks with a single word stranded on the second line. */
  text-wrap: balance;
}
/* The template's signature: first word white, second word lime. */
.title b, .hero b { font-weight: 700; color: var(--accent); }

.lede { font-size: calc(2.4 * var(--u)); line-height: 1.35; margin: 0; }
.body { font-size: calc(1.94 * var(--u)); margin: 0; }
.small { font-size: max(calc(1.32 * var(--u)), 14px); margin: 0; opacity: .82; line-height: 1.36; }
.eyebrow {
  font-size: calc(1.32 * var(--u));
  text-transform: uppercase;
  letter-spacing: .18em;
  color: var(--accent);
  font-weight: 700;
  margin: 0;
}
.body + .body, .small + .small { margin-top: calc(.9 * var(--u)); }
strong { font-weight: 700; }
em { font-style: normal; color: var(--accent); }

/* A short lime rule under a heading, as in the source. */
.rule {
  width: calc(7 * var(--u));
  height: calc(.35 * var(--u));
  background: var(--accent);
  border: 0;
  margin: calc(1.2 * var(--u)) 0 calc(1.6 * var(--u));
}

/* ── the block grid ─────────────────────────────────────────────────────── */

.grid {
  display: grid;
  gap: calc(1.1 * var(--u));
  flex: 1 1 auto;
  min-height: 0;
  grid-template-columns: repeat(var(--cols, 12), 1fr);
  grid-auto-rows: minmax(0, 1fr);
}
.grid--rows2 { grid-template-rows: repeat(2, minmax(0, 1fr)); }
.grid--rows3 { grid-template-rows: repeat(3, minmax(0, 1fr)); }
/* A row of text cards above a wide photo or chart. Equal 1fr rows starve the
   cards, because a chart will happily take half the slide while three
   paragraphs will not fit in what is left. `auto` lets the card row size to
   its content and gives the remainder to the wide element below. */
.grid--auto1 { grid-template-rows: auto minmax(0, 1fr); }

/* Span helpers. Authored compositions set these inline where they are one-offs. */
.c2  { grid-column: span 2; }  .c3  { grid-column: span 3; }
.c4  { grid-column: span 4; }  .c5  { grid-column: span 5; }
.c6  { grid-column: span 6; }  .c7  { grid-column: span 7; }
.c8  { grid-column: span 8; }  .c9  { grid-column: span 9; }
.c12 { grid-column: span 12; }
.r2 { grid-row: span 2; }
.r3 { grid-row: span 3; }

/* ── panels ─────────────────────────────────────────────────────────────── */

.p {
  position: relative;
  padding: calc(1.7 * var(--u));
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: calc(.6 * var(--u));
}
.p--flat { background: var(--panel); }
.p--hi { background: var(--panel-hi); }
.p--lime { background: var(--accent-fill); color: var(--on-accent-fill); }
/* Anything accent-coloured inside the lime panel has to become ink, or it is
   lime on lime. `.num__n` was missing and measured 1.00:1 on screwworm slide 5
   in BOTH themes: the number was simply not there. Pre-existing, found by the
   contrast sweep the light mode needed anyway. */
.p--lime .eyebrow, .p--lime em,
.p--lime .num__n, .p--lime .num__t, .p--lime .stat__l, .p--lime a { color: var(--on-accent-fill); }
.p--bare { padding: 0; }
/* A top-aligned panel does not need symmetric bottom padding, and reclaiming
   it is what keeps a dense three-block panel inside its grid row. */
.p--top { justify-content: flex-start; padding-bottom: calc(1.1 * var(--u)); }
.p--bottom { justify-content: flex-end; }

/* Photo panel: image fills the panel, an optional wash sits over it. */
.p--photo { padding: 0; }
.p--photo > img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* One photograph is shot off level, and the aircraft in it is the reference
   rather than the land behind it: it stands on a mat on flat ground, and its
   own lateral axis measures 1.61deg right-side-low across the red navigation
   LEDs and 1.36deg across the green outboard pair. The distant treeline cannot
   be used -- its three thirds sit at 748, 774 and 761px, which is not a line.
   1.45deg splits the two aircraft readings.
   A rotation has to be counter-scaled or the panel's own corners show through,
   and the factor depends on the panel's proportions: the short, wide panel of
   the document layout needs far more than the tall one on a slide, which is
   why that block below carries its own value. */
.p--photo.is-level > img { transform: rotate(-1.45deg) scale(1.035); }

.p--photo > .p__body {
  position: relative;
  padding: calc(2 * var(--u));
  width: 100%;
}
/* The two washes, exactly as the source uses them: a flat fill at 80% alpha,
   normal blend, over a photograph.

   A LIME WASH NEEDS A HIGH-CONTRAST SUBJECT. At 80% alpha only a fifth of the
   photograph survives, so a pale, low-contrast macro dissolves into a flat lime
   rectangle while a dark subject on a light ground still reads. Measured on the
   real assets: the fly and the rearing-room scene hold up, the larva-on-seed
   macro and the night drone shot do not. Choose the photo to suit the wash
   rather than lowering the alpha, which is what makes it look like the source. */
.wash-ink::after, .wash-lime::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
}
.wash-ink::after { background: var(--wash-ink); }
.wash-lime::after { background: var(--wash-lime); }
.wash-ink > .p__body, .wash-lime > .p__body { z-index: 1; }

/* A WASH IS A DARK SURFACE IN BOTH THEMES, so everything inside one keeps the
   dark treatment. The washes and the scrim sit over photography, and their job
   is to make white type readable on it; they do not follow the page. Without
   this the light theme put a near-black mark and a deep-lime chip on the cover's
   ink wash, which is a dark panel -- unreadable, and invisible to a contrast
   sweep because the wash is a ::after pseudo-element that a background walk
   cannot see. */
.wash-ink, .scrim { --text: var(--paper); --accent: var(--accent-on-dark); }

/* Light mode was retired on 2026-08-21. Every light-scoped rule is gone,
   not merely unreachable, so setting data-theme="light" by hand does
   nothing. This held the light-only 19px cover accent floor.
   git show 8dbb875:decks/assets/deck.css has the removed block. */
.wash-ink, .wash-ink .body, .wash-ink .small, .scrim .small, .scrim .body { color: var(--paper); }
/* Type that sits DIRECTLY on a full-bleed cover photograph rather than inside a
   wash. It is a sibling of the photo, not a descendant, so the rule above cannot
   reach it -- and in light mode it turned to ink over a dark image. Same logic:
   over a photograph, always the light treatment. */
.on-photo { color: var(--paper); }
.wash-lime > .p__body { color: var(--on-accent-fill); }

/* A soft bottom gradient, for text laid over an unwashed photo. */
.scrim::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top, rgba(18,51,42,.92) 0%, rgba(18,51,42,.55) 38%, rgba(18,51,42,0) 72%);
}
.scrim > .p__body { z-index: 1; }

/* ── components ─────────────────────────────────────────────────────────── */

/* Numbered item: lime numeral, bold label, body beneath. */
.num { display: flex; flex-direction: column; gap: calc(.5 * var(--u)); }
.num__n {
  color: var(--accent);
  font-weight: 700;
  font-size: calc(1.94 * var(--u));
  letter-spacing: .04em;
}
.num__t { font-weight: 700; font-size: calc(1.94 * var(--u)); }
/* The three technology slides run their card headings in lime, so the section
   reads as one block. Same pairing as .num__n and .stat__n on this panel. */
.num--lime .num__t { color: var(--accent); }
.num__b { font-size: max(calc(1.32 * var(--u)), 14px); opacity: .86; line-height: 1.34; }

/* Large lime figure with a caption beneath. */
.stat { display: flex; flex-direction: column; gap: calc(.35 * var(--u)); }
.stat__n {
  color: var(--accent);
  font-weight: 700;
  font-size: calc(4.6 * var(--u));
  line-height: 1;
  letter-spacing: -0.02em;
}
.stat__n--sm { font-size: calc(3.2 * var(--u)); }
.stat__l { font-weight: 700; font-size: calc(1.5 * var(--u)); }
.stat__d { font-size: max(calc(1.18 * var(--u)), 13px); opacity: .78; line-height: 1.34; }
.p--lime .stat__n { color: var(--on-accent-fill); }

/* Lime progress bar with a percentage at the end. */
.bar { display: flex; align-items: center; gap: calc(1 * var(--u)); }
.bar__track {
  flex: 1 1 auto; height: calc(.85 * var(--u));
  background: var(--track); border-radius: 999px; overflow: hidden;
}
.bar__fill { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.bar__pct { font-size: calc(1.32 * var(--u)); min-width: calc(6 * var(--u)); }

/* Thin lime outline circle holding a line icon. */
.ic {
  width: calc(4.4 * var(--u)); height: calc(4.4 * var(--u));
  border: calc(.14 * var(--u)) solid var(--accent);
  border-radius: 999px;
  display: grid; place-items: center;
  flex: 0 0 auto;
}
/* Round caps and joins on every icon, set once here rather than per glyph, so a
   hand-drawn set reads as one family. Stroke width is on the 24-unit viewBox, so
   it scales with the circle and stays optically even at any slide size. */
.ic svg {
  width: 58%; height: 58%;
  stroke: var(--accent); fill: none;
  stroke-width: 1.15; stroke-linecap: round; stroke-linejoin: round;
}
/* The reticle glyph runs its ticks out to the edge of its own viewBox, where the
   other three sit inset, so at a shared 58% it would read smaller than the set.
   It gets 66% and a stroke scaled back by the same ratio (1.15 x 58/66 = 1.01),
   which leaves every icon at an identical on-screen weight and leaves the moth
   itself at the same physical size it had before the ticks were lengthened. */
.ic--wide svg { width: 66%; height: 66%; stroke-width: 1.01; }
/* A glyph that is scaled inside the viewBox must counter-scale its stroke, or it
   renders lighter than the rest of the set. 1.01 / 0.76 = 1.33. */
.ic svg .ic__in { stroke-width: 1.33; }
.feat { display: flex; gap: calc(1.4 * var(--u)); align-items: flex-start; }
.feat__x { flex: 1 1 auto; min-width: 0; }

/* Roadmap: accent rule, square nodes, columns, the live column lifted. */
.road { display: flex; flex-direction: column; gap: calc(1.2 * var(--u)); flex: 1 1 auto; min-height: 0; }
.road__line { position: relative; height: calc(1.4 * var(--u)); flex: 0 0 auto; }
.road__line::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: calc(.14 * var(--u)); background: var(--accent); transform: translateY(-50%);
}
.road__nodes { position: absolute; inset: 0; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; }
.road__node { position: relative; }
.road__node::after {
  content: ""; position: absolute; left: calc(2 * var(--u)); top: 50%;
  width: calc(.9 * var(--u)); height: calc(.9 * var(--u));
  background: var(--accent); transform: translateY(-50%);
}
.road__cols { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: calc(1.1 * var(--u)); flex: 1 1 auto; min-height: 0; }
.road__col { padding: calc(1.6 * var(--u)); display: flex; flex-direction: column; gap: calc(.6 * var(--u)); }
.road__col--live { background: var(--panel-hi); }
.road__q { font-size: calc(3.2 * var(--u)); font-weight: 700; line-height: 1; }
.road__col--live .road__q { color: var(--accent); }
.road__t { font-weight: 700; font-size: calc(1.5 * var(--u)); }
.road__b { font-size: calc(1.18 * var(--u)); opacity: .82; }

/* Team row. */
.people { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: calc(1.1 * var(--u)); flex: 1 1 auto; min-height: 0; }
.person { display: flex; flex-direction: column; min-height: 0; }
.person img {
  width: 100%; flex: 1 1 auto; min-height: 0;
  object-fit: cover; object-position: center top; display: block;
}
.person__n { color: var(--accent); font-weight: 700; font-size: calc(1.5 * var(--u)); margin-top: calc(.8 * var(--u)); }
.person__r { font-size: calc(1.18 * var(--u)); opacity: .82; }

/* Pill chip, as on the source cover. */
/* No outline: the wash behind it already separates it from the photograph, and
   the ring was a second frame inside a frame. Lime, matching the headings. */
.chip {
  display: inline-block;
  color: var(--accent);
  padding: calc(.7 * var(--u)) 0;
  font-size: calc(1.94 * var(--u));
  font-weight: 700;
}

/* Open-question item: the deck's most important departure from the template. */
.open { display: flex; gap: calc(1.2 * var(--u)); align-items: flex-start; }
.open__m {
  flex: 0 0 auto; width: calc(.35 * var(--u)); align-self: stretch;
  background: var(--flag);
}
.open__t { font-weight: 700; font-size: calc(1.72 * var(--u)); }
.open__b { font-size: calc(1.32 * var(--u)); opacity: .86; line-height: 1.34; }

/* Data table on a slide. */
.tbl { width: 100%; border-collapse: collapse; font-size: calc(1.32 * var(--u)); }
.tbl th, .tbl td { text-align: right; padding: calc(.62 * var(--u)) calc(.8 * var(--u)); border-bottom: 1px solid var(--hair); white-space: nowrap; }
/* Figures must never wrap, but a prose column will blow the panel width open
   if it cannot. `--wrap` releases the last column only. */
.tbl--wrap th:last-child, .tbl--wrap td:last-child { white-space: normal; line-height: 1.3; text-align: left; }
/* Eight columns of figures will not fit a half-slide panel at the normal size.
   This is the only sanctioned type-size exception, and it applies to the whole
   table so the columns stay in one visual system. */
/* The opposite problem: a short table alone on a slide leaves a void. `--roomy`
   lets four rows carry the panel instead of huddling at the top of it. */
.tbl--roomy { font-size: calc(1.62 * var(--u)); }
.tbl--roomy th, .tbl--roomy td { padding: calc(1.15 * var(--u)) calc(.8 * var(--u)); }
.tbl--tight { font-size: calc(1.05 * var(--u)); }
.tbl--tight th, .tbl--tight td { padding: calc(.5 * var(--u)) calc(.5 * var(--u)); }
.tbl th:first-child, .tbl td:first-child { text-align: left; }
/* One column left-aligned inside a table that is otherwise right-aligned. */
.tbl th.lft, .tbl td.lft { text-align: left; }
/* Three equal columns, so a long cell cannot starve its neighbours. */
.tbl--thirds { table-layout: fixed; }
.tbl--thirds th, .tbl--thirds td { width: 33.333%; }
.tbl thead th { color: var(--accent); font-weight: 700; border-bottom-color: var(--rule); }
.tbl tbody tr:last-child td { border-bottom: 0; }
.tbl .em { font-weight: 700; }
.tbl .flagged { color: var(--flag); }

/* Slide number, bottom right of every interior slide. */
.pageno {
  position: absolute; right: calc(2.4 * var(--u)); bottom: calc(1.6 * var(--u));
  /* .55 measured 3.41:1 in LIGHT and had been failing AA on all 60 slides
     since light mode shipped. Invisible until contrast.mjs learned to fold
     element opacity into the foreground (2026-08-21); before that it scored
     the ink at full strength and passed. .65 is the boundary, so this is the
     smallest change that clears 4.5. */
  font-size: calc(1.18 * var(--u)); opacity: .65; letter-spacing: .06em;
}
.brandmark {
  position: absolute; right: calc(2.4 * var(--u)); top: calc(2 * var(--u));
  height: calc(3.4 * var(--u)); opacity: .9;
}

/* Source line, so any figure can be traced from the slide it sits on. */
.src { font-size: max(calc(1.05 * var(--u)), 12px); opacity: .67; margin: 0; } /* .62 = 4.02:1 in light; .67 clears 4.5 */

/* ── technology columns ─────────────────────────────────────────────────── */
/* Three columns, each a photo above a caption. The caption sets a floor so all
   three are the same height: without it a two-line caption steals height from
   its own photo and that column's image sits visibly shorter than its
   neighbours, which reads as a mistake rather than a choice. */
.techcap { flex: 0 0 auto; min-height: calc(10.4 * var(--u)); justify-content: flex-start; }

/* ── legend swatch ──────────────────────────────────────────────────────── */
/* Used by the capital donut's table, so a row is identified by name AND colour
   rather than by colour alone. The two neutral slots exist because citrus
   greening and the reserve have no series colour in the validated palette. */
.sw {
  display: inline-block; vertical-align: baseline;
  width: calc(1.1 * var(--u)); height: calc(1.1 * var(--u));
  border-radius: 2px; margin-right: calc(.7 * var(--u));
}
.sw--s1 { background: var(--viz-s1); }
.sw--s2 { background: var(--viz-s2); }
.sw--s3 { background: var(--viz-s3); }
.sw--muted { background: var(--viz-muted); }
.sw--res { background: var(--viz-reserve); }

/* ── charts ─────────────────────────────────────────────────────────────── */
/* charts.js is the site's file, loaded unmodified. It reads its colors from
   these custom properties, so the deck only has to restate them for the
   forest ground.

   The site's DARK series palette was re-validated against #12332A with the
   dataviz gate and passes all five checks:
     lightness band · chroma floor · CVD separation (worst ΔE 11.8 deutan)
     · normal-vision floor (ΔE 19.3) · contrast vs surface (all >= 3:1)
   So a chart means the same thing here as it does on the site:
     s1 teal = codling moth · s2 amber = navel orangeworm · s3 violet = screwworm

   The lime is NOT in this list on purpose. It fails the lightness band as a
   data series (L 0.866 against a 0.48-0.67 band), so it stays chrome only. */
.slide {
  --viz-s1: #24a4b1;
  --viz-s2: #c98500;
  --viz-s3: #8168ce;
  --viz-good: #5fd58a;
  --viz-bad: #f08a7e;
  --viz-muted: #3d5a61;
  --viz-track: rgba(255,255,255,.14);
  --viz-grid: rgba(255,255,255,.16);
  /* The capital donut's unallocated wedge. Neither a series nor muted: it is
     "the rest", drawn as a tint of the ground it sits on. */
  --viz-reserve: rgba(255,255,255,.42);
  /* charts.js also draws label text from the site's text/surface tokens. */
  --text: var(--paper);
  --text2: rgba(255,255,255,.86);
  --text3: rgba(255,255,255,.68);
  --bg3: #1D4C38;
  --line: rgba(255,255,255,.2);
  --border2: rgba(255,255,255,.16);
  --bg2: #17402F;
  --m3-font-body: var(--font);
}

/* Light mode was retired on 2026-08-21. Every light-scoped rule is gone,
   not merely unreachable, so setting data-theme="light" by hand does
   nothing. This held the light chart palette at .slide specificity.
   git show 8dbb875:decks/assets/deck.css has the removed block. */
.viz { margin: 0; display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
/* The plot takes the leftover height and the SVG fits INSIDE it. Without the
   height ceiling, `height:auto` sizes the drawing purely from its width, so a
   full-width chart in a short grid row overflows the row by whatever its
   viewBox aspect demands. preserveAspectRatio defaults to "meet", so bounding
   both axes letterboxes rather than distorts. */
.viz .viz-plot { width: 100%; min-height: 0; flex: 1 1 auto; display: flex; }
.viz .viz-plot > svg { width: 100%; height: 100%; max-height: 100%; }
.viz figcaption { font-size: max(calc(1.05 * var(--u)), 12px); opacity: .7; margin-top: calc(.6 * var(--u)); }
/* charts.js sets width="100%" and a fixed px height on every SVG. Forcing
   height:auto lets the viewBox drive the aspect instead, so a chart scales
   with the slide the way the rest of the composition does. Without it a
   260px-tall chart stays 260px on a 4K display and looks stranded. */
.viz-svg { display: block; font-family: var(--font); max-width: 100%; height: auto; }
.viz-tip {
  position: fixed; z-index: 60; pointer-events: none;
  background: var(--void); color: var(--text); border: 1px solid var(--rule);
  padding: 7px 10px; border-radius: 6px; font-size: 12.5px; line-height: 1.45;
  max-width: 280px; box-shadow: 0 6px 22px rgba(0,0,0,.5);
}

/* Slider block, for the live moments (NWS price, CM crossover). */
.ctl { display: flex; align-items: center; gap: calc(1.2 * var(--u)); flex-wrap: wrap; }
.ctl label { font-size: calc(1.32 * var(--u)); font-weight: 700; }
/* Element-level, NOT scoped to .ctl, because the screwworm price slider sits
   outside its .ctl as a full-width flex child of the panel and so escaped both
   halves of this rule.
     · margin-inline: a range input carries a UA `margin: 2px`. On an item whose
       width already fills the panel's content box, that MARGIN box lands 4px
       past it, Chromium counts a descendant margin box in the scrollable
       overflow region, and the panel is overflow:hidden -- so the clip detector
       reported 4px at every width forever with nothing visibly lost. The
       vertical 2px is harmless and stays.
     · accent-color belongs here rather than on the element, or it stops
       following the theme. An inline `accent-color: var(--lime)` held that one
       control lime on a light ground (1.45:1 on white) while its sibling
       correctly turned deep teal. An inline style beats every rule. */
input[type="range"] { accent-color: var(--accent); margin-inline: 0; }
.ctl input[type="range"] { flex: 1 1 auto; min-width: calc(9 * var(--u)); }
.ctl output { color: var(--accent); font-weight: 700; font-size: calc(1.94 * var(--u)); }

/* ── navigation chrome ──────────────────────────────────────────────────── */

/* Centred, not right-aligned. Every slide carries its own number at
   bottom-right (`.pageno`, right: 2.4u / bottom: 1.6u), and a pill in that
   corner sat straight on top of it. The bottom centre is the one edge band no
   slide puts a figure in; the source lines that do run along the bottom clear
   the pill because `.slide__pad` ends above it. */
.nav {
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 16px; z-index: 50;
  display: flex; align-items: center; gap: 4px;
  background: color-mix(in srgb, var(--void) 86%, transparent);
  border: 1px solid var(--rule);
  border-radius: 999px;
  /* The pill is chrome and should read as chrome. It was 147x45, which is
     larger than the counter inside it needs and competes with the slide on a
     cover. Sized down to the ONE hard floor here: WCAG 2.5.8 AA wants a 24px
     target, so the arrow buttons set the height and everything else is packing.
     Do not shrink the button padding further without re-measuring the target. */
  padding: 4px 6px;
  backdrop-filter: blur(8px);
}
.nav button {
  appearance: none; border: 0; background: transparent; color: var(--accent);
  font: inherit; font-size: 13px; line-height: 1;
  padding: 6px 9px; border-radius: 999px; cursor: pointer; /* 13 + 12 = 25px, clears the 24px floor */
}
.nav button:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.nav button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The toggle lives in the nav pill, the only persistent chrome a deck has.
   EXACTLY ONE PER PAGE: the models site has a recorded bug where a second
   #themeBtn made getElementById return the wrong one and left a visible dead
   control. Sized to sit beside the arrows rather than compete with them. */
.nav__theme {
  appearance: none; border: 0; background: transparent; color: var(--accent);
  font: inherit; font-size: 12px; letter-spacing: .06em; text-transform: uppercase;
  padding: 8px 10px; border-radius: 999px; cursor: pointer; line-height: 1;
}
.nav__theme:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.nav__theme:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* 25% smaller than it was (13px -> 9.75px) at the owner's request, and as
   transparent as it can be while still legible: .82 -> .66, not the .615 that
   a straight 25% would give. MEASURED FROM RENDERED PIXELS, light mode, at
   this size: .82 = 7.38:1, .70 = 5.15:1, .66 = 4.50:1, .615 = 4.04:1 against
   a 4.5 floor for text this small. .66 is the boundary. Dark is far easier
   (6.95:1 at .615), so light is the binding case. min-width is scaled with the type or
   the pill keeps its old width and the counter floats in dead space; it exists
   at all so the pill does not resize between "1 / 14" and "14 / 14", which
   tabular-nums alone does not prevent when the digit count changes. */
.nav__count { font-size: 9.75px; color: var(--text); opacity: .67; min-width: 34px; text-align: center; font-variant-numeric: tabular-nums; }

/* Scale rather than width: animating width lays out the page on every frame.
   Same approach as the progress bar on /modular-hangar. */
.progress {
  position: fixed; left: 0; top: 0; height: 3px; z-index: 55;
  background: var(--accent); width: 100%;
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform .25s ease;
}

/* Announced, never seen. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--accent-fill); color: var(--on-accent-fill); padding: 10px 14px; z-index: 99;
}
.skip:focus { left: 8px; top: 8px; }

/* ── phone: the deck becomes a document ─────────────────────────────────── */

/* The document breakpoint sits at 1023px, not 768. Slide mode sizes every tier
   off `--u: 1cqw`, so a 900px window rendered body copy at 10.6px and fine print
   at 9.5px, while one pixel below the old 768 boundary they jumped to 14 and 12.
   The floors above hold the type up, but measured against the clip detector they
   overflow their panels below about 1024px, because the panel shrinks while the
   type does not. So everything under 1024 takes the document layout, which
   already carries fixed px minimums and is the tested path. */
/* SCREEN-scoped deliberately. Chromium lays print out at a width that matches
   this query, so an unscoped `max-width` put the print run into the stacked
   document layout: the video slides printed as a single narrow column, because
   this block's `grid-column: auto !important` outranks anything the print block
   restores. At 768 the bug was latent; raising the boundary exposed it. */
@media screen and (max-width: 1099px) {
  .slide {
    --u: .52rem;
    aspect-ratio: auto;
    width: 100%;
    min-height: 0;
    border-bottom: 1px solid var(--rule);
  }
  /* Show every slide and let the page scroll. This selector must match the
     base rule's specificity (.deck:not(...) .slide), or the base `display:none`
     wins and the reader is stranded on slide 1 with the nav hidden. */
  .deck:not(.deck--flow) .slide { display: block; }
  .slide__pad { padding: calc(3.2 * var(--u)); height: auto; }
  .hero { font-size: calc(5.4 * var(--u)); }
  .grid, .road__cols, .people {
    display: flex; flex-direction: column;
    grid-template-columns: none;
  }
  .grid > *, .road__cols > *, .people > * { grid-column: auto !important; grid-row: auto !important; }
  .p { min-height: calc(14 * var(--u)); }
  .p--photo { min-height: calc(26 * var(--u)); }
  /* A short, wide panel needs a much larger counter-scale for the same angle:
     the rotated corners travel with the LONG side and have to be absorbed by
     the SHORT one. Measured at 900px wide the panel is 847x216, which wants
     1.099 where a slide's 567x523 wants 1.027. */
  .p--photo.is-level > img { transform: rotate(-1.45deg) scale(1.11); }
  /* The headshots are square sources framed for a square crop. Stacking them
     full width against a fixed height made the box landscape, and cover then
     took the chin off every face. Give the box the source's own aspect and
     nothing is cropped at all; cap the width so five of them do not turn the
     slide into a very long scroll. */
  .person { align-items: center; text-align: center; }
  .person img {
    height: auto; flex: 0 0 auto;
    width: min(100%, 260px);
    aspect-ratio: 1 / 1;
    object-position: center;
  }
  .road__line { display: none; }
  /* The deck is a scrolling document here, so the arrows and the counter have
     nothing left to drive, and as of 2026-08-21 the theme toggle is gone from
     every page, so the pill has nothing left to carry at all. Hide it outright
     rather than painting an empty pill with a border and a blur.

     A MALFORMED COMMENT LIVED HERE AND KILLED THIS RULE. Two comments had been
     merged with the first one's closing marker left in place, so the prose
     after it parsed as CSS and the parser dropped the rule that followed. The
     block's other 21 rules recovered, which is why nothing looked broken --
     but the phone rule that hides the slide counter never applied, and a
     reader saw "10 / 10" on a fourteen-slide deck. If a rule in this block
     ever appears to do nothing, check the comment above it first. */
  .nav { display: none; }
  .pageno { position: static; margin-top: calc(1.2 * var(--u)); }
  .brandmark { display: none; }
  .tbl { font-size: calc(1.5 * var(--u)); }
  /* Fine text is sized against the slide width, which on a phone drives it to
     roughly 9px. Detach the smallest tiers from --u here: this is a document
     now, so it takes document minimums rather than slide proportions. */
  .src, .viz figcaption, .chip { font-size: 12px; }
  .small, .num__b, .stat__d, .open__b, .person__r, .road__b { font-size: 14px; }
  .eyebrow, .pageno { font-size: 12px; }
  /* Cover panels carry an inline percentage width so the composition holds on a
     slide. Stacked on a phone that percentage clips the hero, and only
     !important can beat an inline style. */
  .wash-ink, .wash-lime { width: auto !important; }

}

/* ── print: one slide per landscape page ────────────────────────────────── */

/* ── embedded video ─────────────────────────────────────────────────────── */

/* A 16:9 player centred in whatever panel holds it. The poster underneath is
   NOT decoration: an iframe prints blank, so print swaps the two and the slide
   still carries an image on paper. */
.videofig {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--frame);
  overflow: hidden;
}
/* A LIVE VISUALIZATION NEEDS HEIGHT, A VIDEO DOES NOT.

   16:9 is right for a video at any size. It is wrong for the two interactive
   panels on /m3-web-deck: below 1099px the block above turns each slide into a
   document, and at a 390px viewport 16:9 leaves the piece a 337x189 box —
   shorter than the caption-and-rail chrome either one carries. Measured, that
   gave the release device a stage of ZERO height, so it rendered nothing at all,
   and clipped the hangar's caption at 375px tall inside a 189px panel.

   3/4 gives 337x449 instead, which is enough for both, and it is what puts the
   hangar into its own portrait branch (max-aspect-ratio: 1/1) so the caption
   goes full width rather than collapsing into a 57px column.

   IT LIVES HERE, AFTER `.videofig`, AND NOT IN THE PHONE BLOCK ABOVE. Both
   selectors are one class, a media query adds no specificity, and `.videofig`'s
   own `aspect-ratio` is declared further down the file — so the same rule placed
   in the phone block simply loses the cascade and does nothing. It was written
   there first and silently had no effect.

   Scoped to the marker class web-deck/build.mjs adds, so it is INERT on
   /m3-decks, where all three panels are still YouTube and must stay 16:9. */
@media screen and (max-width: 1099px) {
  .videofig--viz { aspect-ratio: 3 / 4; }
}

.videofig iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0; display: block;
}
.videofig__poster { display: none; }

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

@media print {
  html, body { background: #fff; }
  .nav, .progress, .skip, .print-hide, .nav__theme { display: none !important; }

  .deck .slide {
    display: block !important;
    width: 13.333in;
    height: 7.5in;
    aspect-ratio: auto;
    --u: 0.13333in;                 /* 1% of 13.333in, so 1u still means 1% */
    break-after: page;
    break-inside: avoid;
    page-break-after: always;
    page-break-inside: avoid;
    border: 0;
    overflow: hidden;
  }
  .deck .slide:last-child { break-after: auto; page-break-after: auto; }

  /* Backgrounds are the design. Without this the forest ground drops out and
     white text prints onto white paper. */
  * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }

  /* An iframe prints blank, so the poster takes its place. */
  .videofig iframe { display: none !important; }
  .videofig__poster {
    display: block;
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    object-fit: cover;
  }

  /* Undo the phone rules if a narrow window is printing. */
  .grid { display: grid; grid-template-columns: repeat(var(--cols, 12), 1fr); }
  .road__cols, .people { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; }
  .road__line { display: block; }
  .pageno, .brandmark { position: absolute; display: block; }
}

/* Reduced motion. The deck has no slide transitions to suppress: slides swap
   instantly by design. What is left is the progress bar's sweep and the deck
   cards' hover lift, and both are removed rather than shortened, since neither
   carries information the reader needs to see move. */
@media (prefers-reduced-motion: reduce) {
  .progress { transition: none; }
  .card { transition: none; }
  .card:hover, .card:focus-visible { transform: none; }
}

/* ── slide 3: the capacity toggle and the One Health venn ──────────────────
   Both ported from screwworm.m3agtech.com and re-scaled onto --u, because the
   deck sizes everything in container units so a composition scales as one rigid
   object. A rem-sized component dropped in here would not scale with the panel
   and would print at the wrong size. */

/* The toggle that drives DATA.capacity's M3 layer. Owned by the slide; the
   renderer in charts.js finds it by id and wires the click. */
.capsw { display: flex; gap: calc(.6 * var(--u)); margin-top: calc(.9 * var(--u)); flex-wrap: wrap; }
.capsw__b {
  font: inherit;
  font-size: max(calc(1.12 * var(--u)), 12px);
  font-weight: 700;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: calc(.45 * var(--u)) calc(1.1 * var(--u));
  cursor: pointer;
  /* WCAG 2.5.8 wants 24px; the pill is short so the floor is on min-height. */
  min-height: 24px;
}
.capsw__b:hover { border-color: var(--accent); }
.capsw__b.is-on { background: var(--accent-fill); color: var(--on-accent-fill); border-color: transparent; }
.capsw__b:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* One Health venn. The three lobe colours are the site's own and are NOT the
   deck's validated --viz-s1/s2/s3 trio: the triad is a fixed convention rather
   than a data series, so it keeps its own hues and is checked against the panel
   surfaces by contrast.mjs instead. */
.ohv {
  /* Hues adjusted from the source component's, because they sit on a different
     background here. Measured against the deck's real panels, the site's values
     fail the 4.5 bar the contrast sweep holds everything to: light green 2.99,
     light teal 2.82, dark human blue 4.18. Same hue family, moved until each
     clears. Re-measure if --panel moves in either theme.
       light  green #15803d 4.56  ·  blue #2563eb 4.70  ·  teal #186970 5.79
       dark   green #3fcf6e 5.72  ·  blue #7db4ff 5.44  ·  teal #3fc6cf 5.63 */
  --animal: #15803d;
  --human: #2563eb;
  --environmental: #186970;
  /* token aliases, so the ported rules below need no rewriting */
  --color-accent-teal-deep: var(--accent);
  --color-accent-blue: var(--animal);
}
:root[data-theme="dark"] .ohv { --animal: #3fcf6e; --human: #7db4ff; --environmental: #3fc6cf; }
.ohv .venn { width: 100%; height: auto; display: block; overflow: visible; flex: 1 1 auto; min-height: 0; }
.ohv .venn text { font-family: inherit; }
.ohv .pillar[data-pillar="animal"] { color: var(--animal); }
.ohv .pillar[data-pillar="human"] { color: var(--human); }
.ohv .pillar[data-pillar="environmental"] { color: var(--environmental); }
.ohv .pillar .lobe { fill: currentColor; }
.ohv .pillar .ring { stroke: currentColor; fill: none; }
/* The label takes the PANEL's text colour, not the lobe's. Measured on the
   deck's own surfaces the source hues fail AA for 12.5px text in one theme or
   the other: dark human blue 4.18:1, light animal green 2.99:1, light
   environmental teal 2.82:1, all against a 4.5 bar. The lobe it sits inside
   already carries the colour, and every label sits in a single-lobe region, so
   the association is spatial and nothing is lost by making the text legible. */
.ohv .pillar .plabel { fill: var(--text); font-weight: 700; letter-spacing: .5px; }
.ohv .pillar .picon { stroke: currentColor; color: currentColor; }
.ohv .core-disc { fill: var(--accent-fill); }
.ohv .core-t1 { fill: var(--on-accent-fill); font-weight: 700; letter-spacing: 1px; }
.ohv .core-t2 { fill: var(--on-accent-fill); font-weight: 600; }
/* Finished state, always. The source component animates the rings and arcs in
   over 3.4s behind an IntersectionObserver; on a slide that is hidden until it
   is current, that would screenshot and print mid-draw. These are the same
   values the source's own prefers-reduced-motion branch settles on. */
.ohv .arc { fill: none; stroke: var(--animal); stroke-width: 2.4; stroke-linecap: round; opacity: .8; }
.ohv .flow { fill: none; stroke: var(--animal); stroke-width: 2.4; stroke-dasharray: 8 6; stroke-linecap: round; opacity: 1; }

/* The Jamaica note under the venn. Left-bordered callout, the .ohalert shape
   from the source component but on the deck's own tokens: the site version is
   amber on brown, which would need its own contrast validation against these
   panels for no gain. --viz-bad is already validated in both themes. */
.ohnote {
  margin: calc(.6 * var(--u)) 0 0;
  padding: calc(.7 * var(--u)) calc(1 * var(--u));
  border-left: 3px solid var(--viz-bad);
  /* No background fill and no opacity, both deliberately. color-mix() computes
     to color(srgb ...), which tools/contrast.mjs cannot parse: it read the
     0-1 floats as 0-255 and reported 1.53:1 for text measuring 12.45:1. The
     left border carries the callout on its own, and dropping the 0.95 opacity
     costs nothing and stops shaving the real ratio. */
  font-size: max(calc(1.05 * var(--u)), 12px);
  line-height: 1.4;
  color: var(--text);
}
.ohnote strong { color: var(--text); font-weight: 700; }
/* A callout may hold more than one paragraph. They share the block's border and
   padding rather than each carrying their own, which is what keeps a two-part
   note inside a panel that is overflow:hidden. */
.ohnote > p { margin: 0; }
.ohnote > p + p { margin-top: calc(.55 * var(--u)); }
