/* ConvergX. SYSTEM STYLES.
 *
 * Every value resolves through tokens.css. No raw hex, no font name,
 * no framework, no dependency in this file. Load order per page:
 *   <link rel="stylesheet" href="../_system/tokens.css">
 *   <link rel="stylesheet" href="../_system/styles.css">
 *   <script src="../_system/shell.js" defer></script>
 *
 * The system in one line: dots carry the atmosphere, lines carry the
 * structure. Outlined is the default state for everything; a fill is
 * the exception, which is what makes a fill mean something.
 */

/* ===================================================================
   STALE-RATIO WARNING, added 2026-07-30 with the palette swap.

   The accent was a warm orange until 2026-07-30. It is now Webflow blue,
   #146ef5, one value on every surface (see tokens.css).

   Any contrast figure written in a comment BELOW this line, and any
   comment using the word "orange", was computed against the retired
   orange and is STALE. The numbers are wrong now, not merely old.
   tokens.css carries the live, re-measured figures. Trust that file.
   These comments were left rather than mass-edited because rewriting a
   measured claim without re-measuring it is how a wrong number gets
   laundered into a true-looking one. Re-measure, then correct the line
   you actually relied on.
   =================================================================== */

/* PATHS ARE RELATIVE, SITE-WIDE, as of 2026-08-05. Chip: "build it with
 * relative paths so it works no matter where we end up switching it."
 * A url() in a stylesheet resolves against the STYLESHEET's own location, not
 * the page's, so "../assets/..." from /_system/ is correct at every mount
 * point: a root domain, a project subpath, localhost, or file://. That is why
 * these need no per-page treatment the way the HTML does. */

/* ================= 1. RESET ================= */

*, *::before, *::after { box-sizing: border-box; }

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;

  /* Height of the fixed shell chrome: the nav bar plus its rule. Declared
   * once because two places need to agree: the bar sets it, the hero
   * subtracts it to work out how much screen is actually left. */
  --shell-h: 96px;   /* constant. The bar does not resize on scroll */
}

body {
  min-height: 100vh;
  font-family: var(--font-body);
  font-size: var(--step-0);
  font-weight: 400;
  line-height: 1.55;
  background: var(--bg);
  color: var(--fg);
}

img, svg, video { display: block; max-width: 100%; height: auto; }

input, button, select, textarea { font: inherit; color: inherit; }

/* Default document surface if a page forgets to declare one: dark. */
body:not([data-surface]) {
  --bg:            var(--ground);
  --fg:            var(--ink);
  --fg-hi:         var(--ink-hi);
  --fg-lo:         var(--ink-lo);
  --block:         var(--panel);
  --rule-ink:      var(--cool-700);
  --rule-ink-load: var(--cool-600);
  --link:          var(--accent);
  --logo-ink:      var(--ink-hi);
  --dot-ink:       var(--cool-700);
  --lh-lede:       1.40;
}

/* A surface band paints its own ground. Bodies and full-bleed bands
 * both declare data-surface; everything inside inherits. */
[data-surface] { background: var(--bg); color: var(--fg); }

::selection { background: var(--chrome); color: var(--chrome-ink); }

/* ================= 2. TYPE: THE ROLE SCALE ================= */
/* Nine steps, every step with a job, no dead steps. The token clamp
 * values are UNCHANGED; what is authored here is the ASSIGNMENT.
 *
 * The story, loudest to quietest, and each register has one voice:
 *   step-6  hero h1        101px   only inside an authored hero
 *   step-5  default h1      76px   every page that is not a hero
 *   step-4  .display/.pull  57px   the mid-page voice, under h1, over h2
 *   step-3  h2              43px   section head, the workhorse
 *   step-2  .say/h3/index   32px   the emphatic sentence and the sub-head
 *   step-1  lede            24px
 *   step-0  body            18px
 *   step--1 caption/meta    13.5px
 *   step--2 label/tag/th    11px
 *
 * Before this pass step-2 through step-6 carried one element each and
 * the site spoke in two registers: micro-label and giant hero. The
 * middle three gears are where a page argues; they are now occupied.
 *
 * Weight ladder: 400 body, 500 UI and emphasis, 600 headings, 700 hero.
 * Headings are 600 on EVERY ground: a heading must not change weight
 * when the surface changes mid-scroll, and 600 is also the correct
 * light-on-dark compensation, so one value serves both. */

h1, h2, h3, .display {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--lh-heading);
  color: var(--fg-hi);
}

/* Sentence case throughout. The logo is caps; caps headlines compete with it. */
h1 { font-size: var(--step-5); letter-spacing: var(--track-5); max-width: var(--measure-display); }
h2 { font-size: var(--step-3); letter-spacing: var(--track-3); }
h3 { font-size: var(--step-2); letter-spacing: var(--track-2); }

h1, h2 { text-wrap: balance; }

/* The mid-page voice. Below the h1, above the h2, always. */
.display { font-size: var(--step-4); letter-spacing: var(--track-4); max-width: var(--measure-display); }

/* An h1 still carrying class="display" from before the ladder existed
 * would render BELOW its own page's h2 rank. It stays at the h1 step
 * until the class is stripped from the nine pages that still have it,
 * and this rule is still correct after they are. */
h1.display { font-size: var(--step-5); letter-spacing: var(--track-5); }

.display--hero {
  font-size: var(--step-6);          /* exactly once per page */
  font-weight: 700;
  letter-spacing: var(--track-6);
  line-height: var(--lh-hero);
  max-width: var(--measure-hero);    /* ~16 characters: it has to break to dominate */
}

p, li { max-width: var(--measure); }

/* The reset zeroes every margin, and nothing restored the space between
 * paragraphs, so every multi-paragraph column rendered as one fused slab.
 * This single rule is the largest visual fix in the polish pass. Adjacent
 * siblings only: a lone paragraph in a component keeps its zero margin. */
p + p { margin-block-start: var(--space-s); }
/* A claim-row group followed by a plain paragraph rendered fused: `p + p`
 * cannot reach across the .claim wrapper. Same spacing, stated once. */
.claim + p, .claim + .claim, p + .claim { margin-block-start: var(--space-s); }


.lede-text {
  font-size: var(--step-1);
  line-height: var(--lh-lede);   /* 1.34 on paper, 1.40 on dark */
  max-width: var(--measure-lede);
}

/* ---- THE EMPHATIC SENTENCE ----
 * The sentence-level peak, and the answer to "the words should be
 * telling a story through the font sizes". At most ONE per page: it is
 * the single line in the running copy that carries the page's argument.
 * No label, no rule, no number, never in the document outline. A second
 * .say on a page cancels the first, which is why it is greppable. */
.say {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;               /* not 600: it is a sentence, not a heading */
  letter-spacing: var(--track-2);
  line-height: 1.22;              /* read as a sentence, so not the heading mechanism */
  color: var(--fg-hi);
  max-width: var(--measure-lede);
}
p + .say, .say + p, .claim + .say, .say + .claim { margin-block-start: var(--space-m); }

small, .meta { font-size: var(--step--1); }

a {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: var(--rule-hair);
}
a:hover { color: var(--fg-hi); }

/* 600, not 500. At this face 500 against 400 is under 2 percent of glyph width,
 * which is invisible at body size, and it was carrying the whole emphasis job on
 * light surfaces where the colour channel was dead too (see tokens.css). With
 * --fg now stepped back, weight and colour work together instead of neither. */
strong { font-weight: 600; color: var(--fg-hi); }

/* Tertiary action: a text link with a text-character arrow. Not an icon. */
.link-more {
  font-family: var(--font-body);
  font-weight: 500;
  text-decoration: none;
}
.link-more::after { content: " \2192"; }
.link-more:hover { text-decoration: underline; text-underline-offset: 0.2em; }

/* Figures and technical data always in mono with lining tabular figures */
.figure, td.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* ================= 3. MICRO-LABELS ================= */
/* The spec-sheet voice. Sentence case in the DOM, uppercase via CSS,
 * always: screen readers spell DOM capitals letter by letter.
 * Never an eyebrow: a .label is never the immediate previous sibling
 * of an h1, h2 or h3. It sits at a layout edge and carries real data. */

.label {
  font-family: var(--font-mono);
  font-size: var(--step--2);        /* 11px fixed. Never fluid, never smaller */
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  line-height: var(--lh-label);     /* was 1, which collides the moment a label wraps */
  /* BLUE, 2026-07-30, Chip's call: every eyebrow on the site is the accent.
   * --label-ink is surface-scoped and is NOT --accent. A label is 11px, so
   * it needs 4.5:1, and the accent blue is 3.91:1 on the dark ground and
   * 4.02:1 on the muted one. Both fail. Measured across three candidate
   * blues, none clears 4.5:1 on all three grounds, so the token resolves to
   * blue-400 on dark and blue-600 on light and muted. Do not collapse it
   * back to var(--accent): that is the thing that was measured and failed. */
  color: var(--label-ink);
}
.label--hi { color: var(--fg-hi); }
/* Was --fg-lo. Now the same blue as .label: with every eyebrow on the accent,
 * a grey one reads as broken rather than as quiet. The distinction this
 * modifier still carries is the WEIGHT step, 500 not 400, which costs zero
 * reflow because the mono advance is identical at both weights. */
.label--lo { color: var(--label-ink); font-weight: 500; }

/* Mandatory on any label pushed to a right or bottom edge:
 * letter-spacing adds space after the final character, and the
 * negative margin cancels it so the label lines up with the rule. */
.label--edge { display: inline-block; margin-inline-end: calc(var(--track-label) * -1); }

.label--13 {
  font-size: var(--step--1);
  letter-spacing: var(--track-label-13);
}
.label--13.label--edge { margin-inline-end: calc(var(--track-label-13) * -1); }

/* ================= 4. THE LINE SYSTEM ================= */
/* Structure. Derived from the logo's own hairline-and-inline
 * construction. Decorative rules divide content and may be dim.
 * Load-bearing rules are UI component boundaries (WCAG 1.4.11, 3:1)
 * and use --rule-ink-load. When in doubt, load-bearing. */

.rule       { border: 0; border-top: var(--rule-hair) solid var(--rule-ink); }
.rule--load { border-top-color: var(--rule-ink-load); }

/* The inline device, from the logo. Major section breaks and the
 * status-tag underline only. Nothing else. */
.rule-double {
  border: 0;
  height: var(--rule-gap);
  border-top: var(--rule-hair) solid var(--rule-ink);
  border-bottom: var(--rule-hair) solid var(--rule-ink);
}

/* "Where industry lines disappear." Appears EXACTLY ONCE on the whole
 * site, on /about/, beside the tagline. A second instance is a defect. */
.rule-vanish {
  border: 0;
  height: 1px;
  background: linear-gradient(to right, var(--rule-ink) 0%, var(--rule-ink) 55%, transparent 100%);
}

/* ================= 5. THE DOT MATRIX ================= */
/* Atmosphere. Primary texture, per Chip's correction 2026-07-27.
 * Every field needs all three: an edge it terminates against, a named
 * density (and a second field at another density somewhere on the
 * page), and a relationship to the grid or a content boundary.
 * Never scaled, never rotated, never animated, never fixed-attachment.
 * Own element always, so text repaints do not invalidate it. */

.dots {
  background-image: radial-gradient(circle at center, var(--dot-ink) var(--dot-r), transparent 0);
  background-size: var(--tile, var(--dot-mid)) var(--tile, var(--dot-mid));
}
.dots--fine { --tile: var(--dot-fine); }              /* reads as tone */
.dots--mid  { --tile: var(--dot-mid); }               /* default */
.dots--open { --tile: var(--dot-open); --dot-r: 2px; }/* sparse, large empty areas */

/* Staggered lattice: reads angled without a transform (transforms on
 * dot fields produce moire and are banned). */
.dots--stagger {
  background-image:
    radial-gradient(circle at center, var(--dot-ink) 1px, transparent 0),
    radial-gradient(circle at center, var(--dot-ink) 1px, transparent 0);
  background-size: 17px 17px;
  background-position: 0 0, 9px 9px;
}

/* The gradient fade is mandatory on large fields, not optional.
 * Alpha stops written as rgb() so no stray hex keywords enter the file. */
.dots--fade-down {
  -webkit-mask-image: linear-gradient(to bottom, rgb(0 0 0 / 1) 0%, transparent 100%);
          mask-image: linear-gradient(to bottom, rgb(0 0 0 / 1) 0%, transparent 100%);
}
.dots--fade-up {
  -webkit-mask-image: linear-gradient(to top, rgb(0 0 0 / 1) 0%, transparent 100%);
          mask-image: linear-gradient(to top, rgb(0 0 0 / 1) 0%, transparent 100%);
}
.dots--fade-right {
  -webkit-mask-image: linear-gradient(to right, rgb(0 0 0 / 1) 0%, transparent 100%);
          mask-image: linear-gradient(to right, rgb(0 0 0 / 1) 0%, transparent 100%);
}
.dots--fade-left {
  -webkit-mask-image: linear-gradient(to left, rgb(0 0 0 / 1) 0%, transparent 100%);
          mask-image: linear-gradient(to left, rgb(0 0 0 / 1) 0%, transparent 100%);
}
.dots--fade-corner {
  -webkit-mask-image: radial-gradient(circle at 20% 0%, rgb(0 0 0 / 1), transparent 70%);
          mask-image: radial-gradient(circle at 20% 0%, rgb(0 0 0 / 1), transparent 70%);
}

/* ================= 6. LAYOUT ================= */
/* ONE page container, ONE left edge. --page-max is the container and
 * the nav's content edge is the site's single x-position for content,
 * headings, figures and captions. */

.wrap {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* THE UNWRAP.
 * 92 of the 116 .editorial grids sit as a direct child of a .wrap. The
 * wrap already spends the gutter, then the grid's own rail spends it
 * again, so those grids started 62px inside the nav's edge while the 24
 * bare grids started 23px OUTSIDE it. Measured at 1280 before this pass:
 * THREE left edges on the homepage, 62.05 / 85.47 / 147.52. That is the
 * structural half of "things aren't aligned perfectly", and no amount of
 * per-page tuning could have fixed it.
 *
 * A wrap that holds a grid becomes a pass-through, so the grid measures
 * itself against the page and lands on the one edge. The wrap's other
 * children (67 .sec-head blocks) take the same content box by arithmetic
 * rather than by padding, so their rule still spans exactly the grid's
 * 12 columns and stops where the h2 starts.
 *
 * This is the CSS equivalent of moving the .sec-head inside the grid in
 * markup. It is done here because this pass owns no page file. 100% and
 * min(), never 100vw: vw includes the scrollbar and scrolls the page
 * sideways on classic-scrollbar platforms. */
.wrap:has(> .editorial) { max-width: none; padding-inline: 0; }
.wrap:has(> .editorial) > :not(.editorial) {
  width: calc(min(100%, var(--page-max)) - 2 * var(--gutter));
  margin-inline: auto;
}

/* The editorial grid. 12 columns inside rails that resolve to the page
 * edge exactly. --edge is the distance from the viewport to the content;
 * the rail carries it minus the column gap, because the gap sits between
 * the rail and column one and is part of the same distance.
 * minmax(0, ...) throughout: the 0 minimum stops a long unbroken string
 * (a part number, a URL) blowing out a spec-sheet grid. */
.editorial {
  /* REBUILT 2026-08-14 for Safari compatibility. The old template computed
   * its edge tracks with max()/calc() math (viewport-relative clamps
   * inside); some Safari versions drop math functions in grid track lists
   * at computed-value time, which voids the WHOLE template: the grid falls
   * to a single content-sized column pinned left, which is exactly the
   * broken layout two ConvergX reviewers saw. This template contains ZERO
   * math in the track list (pure fr tracks, supported everywhere); the
   * container centres itself with plain width/max-width instead of edge
   * tracks. Line names are unchanged, so every child placement
   * (grid-column: main / full / col-start N) still resolves.
   * TRADE, accepted: grid-column: full now spans the content column rather
   * than the viewport edge (2 usages site-wide). */
  --col-gap: var(--space-m);
  --edge: var(--gutter); /* kept for .bleed--inset */
  display: grid;
  grid-template-columns:
    [full-start main-start] repeat(12, [col-start] minmax(0, 1fr)) [main-end full-end];
  width: calc(100% - 2 * var(--gutter));
  max-width: calc(var(--page-max) - 2 * var(--gutter));
  margin-inline: auto;
  column-gap: var(--col-gap);
  row-gap: var(--editorial-row-gap);
}
.editorial > * { grid-column: main; }
.lede  { grid-column: col-start 1 / span 5; }   /* 5-col lede against the 12-col measure */
.body  { grid-column: col-start 8 / main-end; }
.bleed { grid-column: full; }

/* Adjacent columns align on their first baseline, not on their boxes.
 * A 24px lede and an 18px body starting at the same box top start their
 * type 3px apart, which is exactly the "not aligned" the eye catches and
 * the ruler does not. Load-bearing companion: the margin-zeroing in the
 * rhythm section, so the two columns are spaced by row-gap alone. */
.editorial > .lede,
.editorial > .body { align-self: baseline; }

/* A full-bleed child that still needs its content on the page edge. */
.bleed--inset { padding-inline: var(--edge); }

/* All asymmetry collapses to a single column below 60rem. */
@media (max-width: 60rem) {
  .editorial {
    /* The column gap has nothing left to sit between once the grid is one
     * content column, and it was still being added to the two rail tracks.
     * Any .editorial NOT wrapped in a .wrap (the heroes, the full-bleed
     * figure sections) therefore ran about 42px wider than the viewport
     * and the whole document scrolled sideways. */
    --col-gap: 0px;
    /* minmax(0, 1fr), not 1fr: a bare 1fr is minmax(auto, 1fr) and a
     * content-sized track cannot shrink, which is how /congress/ pushed
     * a 307px hero-head row through a 256px box and overflowed by 19px
     * at 320. */
    grid-template-columns:
      [full-start] var(--gutter) [main-start] minmax(0, 1fr) [main-end] var(--gutter) [full-end];
    column-gap: 0;
  }
  .lede, .body { grid-column: main; }
}

/* ---- 6b. VERTICAL RHYTHM ----
 * The unit is the body line: --space-m equals --step-0 x 1.55 within
 * 3.2% at every viewport width, because the type and space scales share
 * one clamp range. Vertical intervals use only line-commensurate steps:
 * --space-m (1 line), --space-xl (2), --space-3xl (4), --space-4xl (5).
 * --space-s stays the paragraph gap. --space-l and --space-2xl are
 * reserved for horizontal use.
 *
 * THE LAW. A heading belongs to the content below it, so there is more
 * space above a heading than below it, at every level. A boundary is ONE
 * authored interval, never a sum. Space between grid children comes from
 * row-gap alone, never from a margin. */

:root {
  --gap-under-heading:  var(--space-m);    /* 1 line */
  --gap-under-display:  var(--space-xl);   /* 2 lines */
  --editorial-row-gap:  var(--space-xl);   /* 2 lines. Was 2.58, the least
                                            * commensurate step carrying the
                                            * most structural interval */
}

/* The heading rule. 108px of section padding above, 27px below. */
.sec-head { margin-block-end: var(--gap-under-heading); }
/* The one .sec-head that is itself a grid child cannot opt out of
 * row-gap, so it corrects to the same single interval as the other 88.
 * Stated against the gap token so the two can never drift. */
.editorial > .sec-head { margin-block-end: calc(var(--gap-under-heading) - var(--editorial-row-gap)); }

/* A margin on a grid child ADDS to row-gap, it never collapses into it.
 * That is where the emergent 90px and 126px runs came from. */
.editorial > p + p,
.editorial > .claim + p,
.editorial > .claim + .claim,
.editorial > p + .claim,
.editorial > .body { margin-block-start: 0; }

/* Boundaries: asymmetric padding. A section owns the air ABOVE its own
 * content and nothing below, so the interval between two sections is one
 * authored number instead of the sum of two. Twelve accidental boundary
 * values become three: 54 / 108 / 144, which is 2 : 4 : 5 body lines. */
section          { padding-block: var(--space-3xl) 0; }
.section--open   { padding-block: var(--space-4xl) 0; }  /* density-3 zones */
.section--dense  { padding-block: var(--space-xl)  0; }  /* density-8 zones */

/* The double rule is the boundary device itself, so it carries the
 * boundary interval above it and the next section's padding below.
 * All 26 instances are direct children of main, always between two
 * sections, and with sections now top-padded only they would otherwise
 * sit flush against the line of text above. */
main > .rule-double { margin-block-start: var(--space-3xl); }

/* A band paints its own ground, so the ground has to close below its
 * last line. Six pages end in one. Everywhere else the footer's own
 * margin is the closing interval and nothing is stacked on it. */
main > [data-surface]:last-child { padding-block-end: var(--space-3xl); }

/* Mobile: collapse the whole ladder in proportion so the ORDER survives.
 * Before this only --open collapsed, which inverted the ladder and made
 * the site's most open sections its tightest ones. */
@media (max-width: 60rem) {
  section         { padding-block: var(--space-2xl) 0; }
  .section--open  { padding-block: var(--space-3xl) 0; }
  .section--dense { padding-block: var(--space-l)   0; }
  main > .rule-double { margin-block-start: var(--space-2xl); }
  main > [data-surface]:last-child { padding-block-end: var(--space-2xl); }
}

/* ================= 7. SHELL: SKIP LINK, NAV, FOOTER ================= */

/* Visually parked off-canvas until focused. position:absolute is
 * justified here: overlay chrome, not content layout. */
.skip {
  position: absolute;
  left: var(--space-s);
  top: -100%;
  z-index: 40;
  padding: var(--space-2xs) var(--space-s);
  background: var(--focus);
  color: var(--focus-bar);
  font-family: var(--font-mono);
  font-size: var(--step--1);
  text-decoration: none;
}
.skip:focus { top: var(--space-s); }

.shell-header {
  /* No bottom rule. It read as an underline drawn across the page rather than
   * as a boundary, and the header already separates itself: the nav sits on the
   * page ground and the mega panel carries its own edge when it opens. */
  border-bottom: 0;
}
.shell-header .nav-bar {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  min-height: var(--shell-h);
  display: flex;
  align-items: center;
  gap: var(--space-l);
}

.logo { color: var(--logo-ink); display: block; }
/* The mark is a flex item in the nav bar, so it was shrinking below the
 * 150px floor its own clamp declares the moment the links needed room. */
.shell-header .logo { flex: none; }
/* The nav takes the free space, so its LAST item lands on the container's own
 * right edge: the same edge the page grid resolves to, not the viewport's.
 * .nav-bar is already the container (max-width --page-max, --gutter padding),
 * so pushing the nav to the end of it is the whole alignment.
 *
 * This lives on the <nav>, not on .nav-links. .nav-links sits INSIDE the nav,
 * so the auto margin it used to carry only pushed the links to the end of a
 * box that was itself content-sized and parked next to the logo: measured at
 * 1440, the CTA finished 158px short of the text column below it. */
.shell-header .nav-bar > nav { margin-inline-start: auto; }
.logo svg { fill: currentColor; width: 100%; height: auto; }
/* The mark alone, header and footer, every width. The tagline lockup was
 * retired from the header: see the note in shell.js buildHeader. The
 * artboard for it is 286.7 x 86.45 if it is ever wanted back. */
.logo--mark   { width: clamp(150px, 18vw, 240px); }  /* floor 150px: below it the inline strokes close up */
/* Clear space: 12.5% of rendered width on every side (the cap height). */
.logo-clear { padding: 12.5%; }

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-m);
  list-style: none;
  padding: 0;
}
/* Direct children only. The Industries panel hangs inside this list, so
 * a descendant selector here reaches into the panel and, for instance,
 * puts the current-page inline device under a link that is not the nav
 * item. The bar styles nav items; nav items are `.nav-links > li > *`.
 *
 * The mega trigger is a button, not a link, so every nav-bar rule that
 * governs a nav item names it too. Same item, same look. */
.nav-links > li > a,
.nav-links > li > .nav-mega-btn {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--step--1);
  color: var(--fg);
  text-decoration: none;
  padding-block: var(--space-2xs);
  white-space: nowrap;   /* the bar is one line at desktop, so are its labels */
}
.nav-links > li > a:hover,
.nav-links > li > .nav-mega-btn:hover { color: var(--fg-hi); }

/* About had its own rule here: an 11px mono uppercase micro-label, one tone
 * back, to mark it as a utility link rather than a primary item. Retired
 * 2026-07-28 (Chip). It reads as a primary item now and takes NO rule at all,
 * because the bar rules above already give it the right treatment. The
 * .nav-util class went with the rule; a hook with nothing hanging off it is
 * the next person's dead end. What still separates About from the items
 * beside it is structural: no panel, no caret, last in the row. */

/* The header CTA is a BUTTON that happens to be an <a>, so the nav-link rule
 * above must not claim it. `.nav-links > li > a` outranks `.btn--solid` on
 * specificity and was repainting the label --fg on the orange fill: measured
 * 1.27:1, a hard contrast failure that looked merely "a bit washed out".
 * Restated here at matching specificity. The ink on an accent fill is
 * --accent-ink, white on the Webflow blue, 4.59:1. Never hardcode it. */
.nav-links > li > a.btn--solid {
  color: var(--accent-ink);
  font-weight: 700;
}
.nav-links > li > a.btn--solid:hover { color: var(--accent-ink); }
.nav-links > li > .nav-mega-btn[aria-current] { color: var(--fg-hi); }
/* Current page: the inline device, not a colour change. */
.nav-links > li > a[aria-current="page"],
.nav-links > li > .nav-mega-btn[aria-current] .nav-mega-label {
  color: var(--fg-hi);
  /* ONE hairline, not two, and drawn as an absolutely positioned pseudo so the
   * active item occupies exactly the same box as an inactive one. The previous
   * version used a border plus a block ::after and added padding-bottom to make
   * room, which nudged the item down the moment it became current. Nothing here
   * is in the flow, so nothing shifts. */
  position: relative;
}
.nav-links > li > a[aria-current="page"]::after,
.nav-links > li > .nav-mega-btn[aria-current] .nav-mega-label::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--rule-gap) * -1);
  border-bottom: var(--rule-hair) solid var(--fg-hi);
}

/* Mobile nav: native details/summary disclosure. No JS menu behaviour.
 *
 * Direct children throughout. The Industries disclosure nests a second
 * details, a second list and a second set of links inside this one, and
 * a descendant selector here reaches all of them: the menu bar's mono
 * uppercase caps and its inverted open state would land on a nested
 * summary that is a nav row, not a menu bar. */
.nav-mobile { display: none; }
.nav-mobile > summary {
  list-style: none;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--fg);
  padding: var(--space-2xs) var(--space-s);
  border: var(--rule-hair) solid var(--rule-ink-load);
}
.nav-mobile > summary::-webkit-details-marker { display: none; }
.nav-mobile[open] > summary { background: var(--fg); color: var(--bg); }
.nav-mobile > ul {
  list-style: none;
  padding: var(--space-s) 0 0;
}
.nav-mobile > ul > li { border-top: var(--rule-hair) solid var(--rule-ink); }
.nav-mobile > ul > li > a {
  display: block;
  padding: var(--space-s) var(--space-2xs);
  color: var(--fg);
  text-decoration: none;
  font-weight: 500;
}
.nav-mobile > ul > li > a[aria-current="page"] { color: var(--fg-hi); }

/* ---- 7b. THE INDUSTRIES PANEL ----
 * A disclosure, not a menubar. Hairlines and surface tokens only: no
 * new colour, no shadow, no radius, no card. It is the nav bar's own
 * ground extended downwards and stopped with a rule.
 *
 * MOTION: none. The panel appears. `hidden` is the whole mechanism, so
 * it is also correct with CSS disabled and correct for reduced motion
 * without a query. */

/* Sticky, and it is ALSO the mega panel's containing block. `position: sticky`
 * establishes one exactly as `relative` did, so the panel keeps resolving
 * against the bar and not against a menu item. Do not move this to a child.
 *
 * The background is not optional: with no bottom rule and a transparent header,
 * page content scrolls visibly underneath it. z-index sits above the page and
 * above .mega (9), and below .skip, which is raised to 40 so the skip link can
 * still appear over a stuck bar. */
.shell-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--bg);
  /* The rule is ALWAYS in the box and only changes colour, so the header never
   * changes height and the page never shifts by the 1px a real border would add.
   * Transparent at rest, because at the top of the page the bar is part of the
   * composition and a line under it read as a line drawn across the page. Inked
   * the moment it floats, because then it genuinely is separating itself from
   * content moving underneath it. */
  border-bottom: var(--rule-hair) solid transparent;
  transition: border-color var(--t) linear;
}
.shell-header--floating { border-bottom-color: var(--rule-ink); }



.nav-mega-btn {
  background: none;
  border: 0;
  padding-inline: 0;
  cursor: pointer;
  /* inline-flex, so the current-branch rule can live on .nav-mega-label
   * and hug the WORD instead of running the whole control width and
   * swallowing the caret. The rule is drawn as a border plus a block
   * ::after, and a block inside an inline span pushes the next sibling
   * onto a new line, which dropped the caret. As flex items the two
   * cannot do that to each other. Baseline alignment keeps the caret
   * sitting on the word's baseline, not on the rule below it. */
  display: inline-flex;
  align-items: baseline;
  gap: 0.45ch;
}
/* De-emphasis token on a decorative marker, never on the label itself.
 * MOBILE ONLY as of 2026-08-04. The desktop bar's caret button was removed on
 * Chip's instruction once a caret on every panel read as clutter, and the
 * disclosure moved onto the link (see shell.js). The class survives because the MOBILE
 * nav still uses it, on a <span> inside a <summary>, and that one keeps its
 * arrow deliberately: a <summary> is a disclosure widget rather than a link,
 * so with no affordance a reader has nothing telling them it opens.
 * These rules are therefore not dead. Deleting them unstyles the mobile
 * caret; check both surfaces before touching this block. */
.nav-mega-caret {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  color: var(--fg-lo);
}

.mega {
  position: absolute;
  inset-inline: 0;
  top: 100%;
  z-index: 9;                  /* over the page, under .skip at 10 */
  background: var(--bg);
  border-bottom: var(--rule-hair) solid var(--rule-ink-load);
  /* A panel taller than the viewport loses its last rows outright once the
   * header is stuck, because then the panel is pinned to the viewport and
   * there is nothing left to scroll to reach them. The Conference panel with
   * its new rows measures 667px, which clears a 1280x860 window and clips the
   * last row on a 1280x720 one. Capping and scrolling costs nothing when the
   * panel fits, which is every panel at every common height today, and it is
   * what keeps the next set of rows from silently amputating a link. */
  max-height: calc(100vh - var(--shell-h));
  overflow-y: auto;
}
.mega[hidden] { display: none; }   /* beats the grid below, which would revive it */

.mega-inner {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-l) var(--gutter);
  display: grid;
  grid-template-columns: 2fr 1fr;   /* the verticals lead; the split is the aside */
  gap: var(--space-l) var(--space-2xl);
}
/* The Industries panel is one FULL-WIDTH column, because its single column
 * holds a grid. In the default 2fr 1fr it would lay that grid out inside two
 * thirds of the panel and leave the last third empty. */
.mega-inner--1 { grid-template-columns: 1fr; }

.mega-col > .label { display: block; margin-block-end: var(--space-s); }
/* A second column that is genuinely an aside to the first, divided by a rule.
 * Was .mega-col--sides, named for the two-path chooser that used to be the
 * only thing in it. That chooser left the Industries panel on 2026-07-29 and
 * the Conference panel's "not the Congress" column took the slot, so the name
 * described nothing that used it. Nothing outside _system ever referenced it. */
.mega-col--aside {
  border-inline-start: var(--rule-hair) solid var(--rule-ink);
  padding-inline-start: var(--space-l);
}
/* One step down from the page-level .link-index: this is chrome sitting
 * over content, not the page's own index, and at --step-1 the four rows
 * ran most of a laptop screen. */
.mega .link-index a { font-size: var(--step-0); }
.mega .link-index li { padding-block: var(--space-xs); }
.mega .link-index li:first-child { border-top: 0; }   /* the label is the boundary */
/* Was a hand-tuned 52ch. Same intent as the body cap, so one source. */
.mega .link-index .descriptor { max-width: var(--measure); }
.mega-all { margin-block-start: var(--space-m); }

/* ---- THE INDUSTRIES GRID ----
 * The same .link-index rows, laid out in columns instead of one stack.
 *
 * THE COLUMN COUNT IS A LAYOUT CONSTANT AND ENCODES NO COUNT OF ANYTHING.
 * Three across is what Chip asked for and what a nav-panel cell can hold at
 * this measure; the sectors flow into it, so a sector arriving or being pulled
 * changes nothing here and nothing in shell.js. Deliberately NOT auto-fit with
 * a minmax floor: at 1920 the panel is wide enough that auto-fit would find a
 * fourth column and the grid would silently stop being three across.
 *
 * Column gap only. With no row gap the rows sit directly under one another and
 * each cell's own border-top draws one continuous hairline across the row,
 * which is the same separator the stacked list already uses. */
.link-index--grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  column-gap: var(--space-2xl);
}
/* The panel only exists above 60rem, where the bar stops being a phone menu.
 * Between there and 68rem three cells are too narrow for a two-line
 * descriptor, so the grid drops a column and the rows reflow. */
@media (max-width: 68rem) {
  .link-index--grid { grid-template-columns: 1fr 1fr; }
}
/* .mega kills the first row's top border because the column label is the
 * boundary. In a grid that only unsets ONE of the cells on the top row, so the
 * rule is put back and the label keeps its own margin as the separator. */
.mega .link-index--grid > li:first-child {
  border-top: var(--rule-hair) solid var(--rule-ink);
}

/* The promo cell. Not a sector and not counted: the prompt leads, the call
 * closes. Both inner spans are existing components, so this rule is spacing
 * only and states no colour, size or face of its own. */
.mega-promo > .label { display: block; margin-block-end: var(--space-3xs); }
.mega-promo > .link-more { display: inline-block; margin-block-start: var(--space-xs); }

/* ---- 7c. THE PHONE VERSION ----
 * A mega panel is wrong on a phone, so below the breakpoint the same
 * content is a native details disclosure nested in the existing menu.
 * Zero script, zero motion, and the four industries plus both paths are
 * one tap from the menu rather than two screens away. */

.nav-sub summary {
  display: block;                 /* also removes the marker in Firefox */
  list-style: none;
  cursor: pointer;
  padding: var(--space-s) var(--space-2xs);
  color: var(--fg);
  font-weight: 500;
}
.nav-sub summary::-webkit-details-marker { display: none; }
.nav-sub[open] summary { color: var(--fg-hi); }
/* The indent is what says "inside". */
.nav-sub > :not(summary) { padding-inline-start: var(--space-s); }
.nav-sub .link-index li { padding-block: 0 var(--space-xs); }
.nav-sub .link-index a {
  display: block;                                   /* the row is the target */
  font-size: var(--step-0);
  padding-block: var(--space-s) var(--space-3xs);   /* holds the 44px target */
}
.nav-sub .two-path { border-top: 0; margin-block-start: var(--space-s); }
.nav-sub .two-path > a { padding: var(--space-s) var(--space-2xs); }
.nav-sub .nav-sub-all { padding-block: var(--space-s); }

@media (max-width: 60rem) {
  .nav-links { display: none; }     /* takes .nav-mega and its panel with it */
  .nav-mobile { display: block; margin-inline-start: auto; }
  .shell-header .nav-bar { flex-wrap: wrap; align-items: center; }
  .nav-mobile > ul { width: 100%; }
}

/* ---- 7d. THE NOTICE BAR ----
 * One line of accent blue above the header. It is not a surface: it carries the
 * accent as a fill and states its own ink, so it renders identically on the
 * dark, light and muted grounds rather than inheriting three different sets of
 * tokens and three different results. That is deliberate for a fill: the
 * surface system exists so components sit ON a ground, and this one IS a
 * ground.
 *
 * CONTRAST, and it is the whole reason --accent-ink exists. --accent-ink on
 * --accent measures 5.76:1, clearing AA for normal text at any size. Light
 * type on this orange measures 3.42:1 and FAILS. Never invert this, never
 * substitute --fg or --fg-hi, and note that the header CTA a few rules above
 * has this exact note for this exact reason.
 *
 * Focus inside the bar is the global two-part indicator: the yellow alone is
 * 2.55:1 on orange, and the --focus-bar underneath it is 5.76:1, which is the
 * part that carries it. Same composite that already carries focus on the
 * orange CTA and on every light surface. */
/* Back to var(--accent), 2026-07-30 second pass. This was briefly pinned to
 * a literal, because the first palette had no single colour that worked as a
 * fill on both grounds and the accent had to be surface-scoped, which would
 * have made this same bar two different colours across the site. Webflow blue
 * removed that problem (3.91:1 on the dark ground, 4.59:1 on paper), so the
 * bar is one token again. White on the blue measures 4.59:1. */
.notice {
  background: var(--chrome);
  color: var(--chrome-ink);
}
.notice-inner {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-2xs) var(--gutter);
  font-size: var(--step--1);
  font-weight: 500;
}
/* The global link rule paints --link, which is the accent itself on dark and
 * near-invisible here, and :hover repaints --fg-hi, which is LIGHT on dark and
 * would drop the link to 3.42:1 on hover alone. Both restated. The underline
 * is what marks it as a link, not a colour change, because there is no second
 * safe ink on this fill. */
.notice-inner a {
  color: var(--chrome-ink);
  font-weight: 700;
}
.notice-inner a:hover { color: var(--chrome-ink); }

.shell-footer { margin-top: var(--space-4xl); }
.shell-footer .footer-inner {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-2xl) var(--gutter) var(--space-xl);
}
/* 11rem HOLDS FIVE COLUMNS at --page-max, which is the ceiling the footer is
 * kept under. When the column set once ran past it, About wrapped onto a row
 * of its own. The fix was NOT to narrow this minimum: Chip's call was to drop
 * the Industries column instead, so every column keeps a width its labels fit
 * in. See the FOOTER array in shell.js for why dropping Industries costs no
 * navigability. If another column is ever genuinely needed, reconsider the
 * column set before reconsidering this number. */
.footer-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-l);
  margin-block-start: var(--space-xl);
}
.footer-cols ul { list-style: none; padding: 0; }
.footer-cols li { padding-block: var(--space-3xs); }
.footer-cols a {
  color: var(--fg);
  text-decoration: none;
  font-size: var(--step--1);
}
.footer-cols a:hover { color: var(--fg-hi); text-decoration: underline; }
.footer-meta {
  margin-block-start: var(--space-xl);
  padding-block-start: var(--space-s);
  border-top: var(--rule-hair) solid var(--rule-ink);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s) var(--space-l);
  justify-content: space-between;
}

/* ================= 8. BUTTONS ================= */
/* Outlined is the default. At most ONE solid accent fill per page
 * (one filled panel or one filled button, not both). */

.btn {
  display: inline-block;
  padding: var(--space-xs) var(--space-l);
  border: var(--rule-hair) solid var(--rule-ink-load);
  border-radius: var(--radius);
  background: transparent;
  color: var(--fg-hi);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--step--1);
  text-decoration: none;
  cursor: pointer;
}
.btn:hover  { border-color: var(--fg-hi); }
.btn:active { background: var(--fg); color: var(--bg); border-color: var(--fg); }

.btn--solid {
  background: var(--accent);
  color: var(--accent-ink);     /* white on the blue fill, 4.59:1. Never hardcode it */
  border-color: var(--accent);
  font-weight: 700;
}
.btn--solid:hover  { background: var(--accent-text); border-color: var(--accent-text); }
.btn--solid:active { background: var(--fg-hi); border-color: var(--fg-hi); color: var(--accent-ink); }

.btn:disabled, .btn[aria-disabled="true"] {
  color: var(--fg-lo);
  border-color: var(--rule-ink);
  background: transparent;
  cursor: not-allowed;
}


/* ================= 8b. WHERE BLUE IS ALLOWED ================= */
/* Chip, 2026-08-05: too much blue. Blue is now RATIONED TO TWO JOBS and every
 * other blue surface became chrome.
 *
 * BLUE KEEPS:
 *   .btn--solid          an action a reader can take
 *   .vid-play            same, it is a play control
 *   .flow-hub, --flow-ink, the globe, figure lines and nodes
 *   .rl-steps > .rl-in   a marked band inside a data figure
 *   --link               a link is an action, and blue is the one affordance a
 *                        reader never has to be taught. Greying links would cut
 *                        blue further and cost more than it saved.
 *
 * BLUE LOST, all now chrome:
 *   .notice              a full-bleed band across the top of every page
 *   .launcher            the filled boxes on the homepage
 *   .app-band            a full-bleed band on the Congress page
 *   .block--accent, the photo hero scrim,
 *   .outcome-list markers, ::selection, and every eyebrow site-wide
 *
 * THE INK FLIPS WITH THE FILL. Blue is dark, so it took white type. Chrome is
 * mid, so it takes DARK type: 5.23:1 against --ink-900, where white would be
 * 3.26:1 and fail. Any component moved to chrome must move its ink too, which
 * is why these are paired below rather than left to a token swap. */

/* ================= 9. BLOCKS ================= */
/* Not "cards": no shadow, no radius, no colour strip. An outlined
 * block by default; a panel block when it needs a ground of its own. */

.block {
  border: var(--rule-hair) solid var(--rule-ink-load);
  padding: var(--space-l);
}
.block--panel {
  border: 0;
  background: var(--block);
  padding: var(--space-l);
}

/* The one sanctioned solid-accent panel form. Counts as the page's single
 * accent fill. Type on it is --accent-ink, white, measured 4.59:1. */
.block--accent {
  border: 0;
  background: var(--chrome);
  color: var(--chrome-ink);
  padding: var(--space-l);
}
.block--accent h2, .block--accent h3, .block--accent .label { color: var(--chrome-ink); }

/* ================= 10. STATUS TAGS: RETIRED ================= */
/* 2026-07-29, CHIP'S DECISION. Every module status tag was removed from
 * the site, so .tag and .tag--edge styled nothing and the rules were
 * deleted rather than left orphaned. Do not re-add them, and do not add
 * any substitute status treatment, without Chip. */

/* ================= 11. MODULE INDEX ================= */
/* Subgrid so title and description align across every module regardless
 * of description length. Density-8 zone: no card containers, hairlines
 * separate data. */

.modules {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 0 var(--space-l);
}
/* auto-fit resolved to four columns at desktop, which put the module
 * rows on a rhythm the page grid does not have and left fifteen modules
 * in three-and-three-quarter rows. Three columns snap to the twelve and
 * give five clean rows. Same column gap as the grid it now sits on. */
@media (min-width: 60.0625rem) {
  .modules { grid-template-columns: repeat(3, 1fr); column-gap: var(--space-m); }
}
.module {
  display: grid;
  grid-row: span 2;                   /* h3 + description. Was 3 with the tag. */
  grid-template-rows: subgrid;
  row-gap: var(--space-xs);
  container-type: inline-size;
  padding-block: var(--space-m);
  border-top: var(--rule-hair) solid var(--rule-ink);
}
.module h3 {
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: var(--track-1);
}
.module p { font-size: var(--step--1); color: var(--fg); }

/* Portraits. /about/leadership/ ONLY, and it is the only page on the site
 * that shows a face: FIGURE-SYSTEM 4.1 carries a page-scoped exception for
 * it and says in as many words not to extend it. Do not reuse this class.
 *
 * Three rows instead of two: portrait, name, role.
 *
 * ONE size for all ten, founder included, and no hover, no lightbox, no
 * expand, no per-person link, no bio, no honorific.
 *
 * PARTLY REVERSED 2026-07-31 ON CHIP'S INSTRUCTION, and the reversal is
 * recorded here rather than by rewriting the line above, so nobody
 * re-derives the old rule from a comment that no longer matches the page.
 * What changed: every name is now a link opening a full-page overlay
 * (component at section 39). What did NOT change, and what still holds the
 * roster: ONE size for all ten, the founder included, in ConvergX's own
 * published order, with the same trigger, the same overlay and the same
 * fields on every one of them. NO BIOGRAPHY IS RENDERED FOR ANY TEAM
 * MEMBER. _reference/CONVERGX-CO-2026-07-29.md section 3.13 captures only
 * excerpted fragments of their bios, labelled "verbatim or near", and
 * assembling prose out of those would be writing a biography rather than
 * quoting one. The overlay carries photograph, name, role and the entity
 * ConvergX attributes them to. Nothing else, and nothing invented.
 *
 * THAT is what holds the
 * page to a roster rather than a founder and some staff, and it holds at
 * any size. An earlier pass here capped the frame at --space-4xl on the
 * theory that small portraits read as an index and large ones read as a
 * gallery. CHIP OVERRULED IT 2026-07-29 and he is right: key-person risk
 * comes from DIFFERENTIAL treatment, not from scale. Ten identical frames
 * in one flat grid in the order ConvergX publishes them is a roster
 * whether they are 130px or 440px. Do not re-cap the size. Do add to the
 * list above if a hover, a link or a bio is ever proposed.
 *
 * Full column width, so no size value is authored at all and the frame is
 * whatever the existing three-column grid already gives it. MEASURED: the
 * widest column the site can produce is at the 1920 breakpoint where
 * --page-max goes to 1360, and it lands under the 640px source, so no
 * portrait is ever upscaled by CSS. If a future breakpoint takes a column
 * past 640 these files must be re-fetched larger, not stretched.
 *
 * --radius is 0 and declared structural, so the frame stays square. A
 * circle would be the only rounded corner in the system and it would
 * import a team-page convention this page is deliberately not.
 *
 * object-fit only ever matters if a future file arrives non-square; it
 * crops rather than distorting the row.
 *
 * align-content: start is load-bearing, not tidying. MEASURED 2026-07-29:
 * `grid-template-rows: subgrid` on .module resolves to nothing, because
 * .modules declares no explicit row tracks and a subgrid cannot draw from
 * implicit tracks its own span created. The rows fall back to auto, the
 * parent band fixes the module height, and the leftover is then split
 * EQUALLY across the three auto rows, so a module with a three-line role
 * got a shorter portrait row than its neighbours and its name sat 7px
 * high. start pins the rows to the top and parks the leftover at the
 * bottom of the module, where nothing is drawn.
 * The same fallback is why the two-row modules elsewhere on the site do
 * not align their descriptions either. That is a pre-existing defect in
 * the shared component, it is NOT fixed here, and fixing it means giving
 * .modules real row tracks, which changes every page that uses it. */
.modules--people > .module { grid-row: span 3; align-content: start; }
.module-portrait { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; }

/* ================= 12. CLAIM BLOCK AND PULL STATEMENT ================= */

.claim { border-top: var(--rule-hair) solid var(--rule-ink-load); padding-block-start: var(--space-m); }
.claim .claim-proof {
  font-size: var(--step--1);
  color: var(--fg-lo);
  margin-block-start: var(--space-xs);
}

/* Pull statement: display type, no quotation-mark ornament,
 * attribution on its own line with a plain hyphen.
 *
 * grid-column: main is load-bearing, not cosmetic. A step-5 line dropped
 * into a 5-column track broke to one and two words a line, eight lines
 * deep. A pull owns the full measure wherever it lands. */
.pull { padding-block: var(--space-2xl); grid-column: main; }
/* The pull's own padding separates it from what sits above. When nothing
 * does, the section's padding is already the separation and stacking both
 * produces the dead vertical run this pass exists to remove. */
.pull:first-child { padding-block-start: 0; }
/* step-4, not step-5. A pull is the mid-page or closing statement and it
 * sits between the h1 and the h2, exactly the rank it holds. At step-5 it
 * tied or beat the page title on two pages: a borrowed quote outranking
 * the page's own subject. It cannot rival a title again by construction. */
.pull p:first-child {
  font-family: var(--font-display);
  font-size: var(--step-4);
  font-weight: 600;
  letter-spacing: var(--track-4);
  line-height: var(--lh-heading);
  color: var(--fg-hi);
  max-width: var(--measure-display);
}
.pull .attribution {
  margin-block-start: var(--space-m);
  font-size: var(--step--1);
  color: var(--fg-lo);
}

/* A caption under a display line needs more air than the paragraph
 * default gives it. Same family as .pull .attribution, same reasoning. */
.display + .meta { margin-block-start: var(--space-m); }

/* Inline placeholder marker. Visually loud, greppable: the literal string is
 * the point.
 *
 * THE OLD COMMENT HERE CLAIMED IT WAS "never shipped". That was not true. The
 * shell rendered one in the footer of every page on the site, naming the
 * client's CEO in an internal to-do note, and pages carried around twenty
 * more. All of them were swept on 2026-07-29.
 *
 * The rule stays because /_dev/ still uses it, and /_dev/ is the one place it
 * is FOR. The claim it may not carry again is that nothing renders it: check,
 * do not assume. `grep -rn 'class="ph"' site` is the check, and any hit
 * outside site/_dev/ is a defect, not a work-in-progress marker. */
.ph {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  letter-spacing: 0;              /* stops label tracking leaking in from a parent */
  color: var(--link);
  border: var(--rule-hair) dashed var(--link);
  padding: 0 var(--space-2xs);
  word-break: break-word;
}

/* ================= 13. LINK INDEX AND TWO-PATH CHOOSER ================= */

.link-index { list-style: none; padding: 0; }
.link-index li {
  border-top: var(--rule-hair) solid var(--rule-ink);
  padding-block: var(--space-s);
}
/* step-2, not step-1. On the router pages the destinations ARE the
 * payload, and at lede size they read as a list of links under the
 * heading rather than as the thing the page is for. Display tracking on
 * body-size link text was also the one place --track-display violated
 * its own "never on body" comment. */
.link-index a {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: var(--track-2);
  color: var(--fg-hi);
  text-decoration: none;
}
.link-index a:hover { color: var(--link); }
.link-index .descriptor {
  display: block;
  font-size: var(--step--1);
  color: var(--fg);
  max-width: var(--measure);
}

/* Foot-of-page chooser: the only place both sides appear at equal
 * weight, and never a hero. */
.two-path {
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-top: var(--rule-hair) solid var(--rule-ink-load);
}
.two-path > a {
  padding: var(--space-l);
  text-decoration: none;
  color: var(--fg-hi);
  border-inline-end: var(--rule-hair) solid var(--rule-ink-load);
}
.two-path > a:last-child { border-inline-end: 0; }
.two-path > a:hover { background: var(--block); }
.two-path .label { display: block; margin-block-end: var(--space-xs); }
@media (max-width: 60rem) {
  .two-path { grid-template-columns: 1fr; }
  .two-path > a { border-inline-end: 0; border-block-end: var(--rule-hair) solid var(--rule-ink-load); }
  .two-path > a:last-child { border-block-end: 0; }
}

/* ================= 14. DATA TABLE ================= */
/* Hairline row separation only. No zebra fill, no double borders. */

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
}
caption {
  text-align: left;
  padding-block-end: var(--space-s);
}
th {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  line-height: var(--lh-label);   /* was inheriting the 1.55 body line unit */
  text-align: left;
  color: var(--fg-lo);
  padding: var(--space-xs) var(--space-s) var(--space-xs) 0;
  border-bottom: var(--rule-hair) solid var(--rule-ink-load);
}
td {
  padding: var(--space-xs) var(--space-s) var(--space-xs) 0;
  border-bottom: var(--rule-hair) solid var(--rule-ink);
  vertical-align: top;
}
td.num { text-align: right; }
th.num { text-align: right; }

/* A row header is a NAME, not a column heading. Taking the th treatment
 * gave every price and spec row a mono grey underlined label that read as
 * a link or a tag, and a heavier bottom rule than the cell beside it, so
 * each row's hairline visibly stepped in tone and position. Column headers
 * keep the mono caps; row headers read as body. */
tbody th[scope="row"] {
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  line-height: var(--lh-body);   /* a row header is body text, not a label */
  color: var(--fg);
  border-bottom-color: var(--rule-ink);
  /* The UA default for a th is middle, and only `td` above restated it.
   * A two-column table never exposed that, but the register table's third
   * column wraps to three lines at 375 and floated the pass name to the
   * vertical centre of its own row. A row header names the row, so it sits
   * on the row's first line. */
  vertical-align: top;
}

/* ================= 15. FORMS ================= */
/* Label above input, always. Never placeholder-as-label. Helper text
 * present in markup. Error below the input, associated, and worded:
 * colour is never the only channel. */

.field { margin-block-end: var(--space-l); }
.field label {
  display: block;
  font-weight: 500;
  color: var(--fg-hi);
  margin-block-end: var(--space-2xs);
}
.field .helper {
  font-size: var(--step--1);
  color: var(--fg-lo);
  margin-block-end: var(--space-2xs);
  max-width: var(--measure);
}
/* The list is an allowlist, so a type missing from it renders as an unstyled
 * browser default beside styled siblings. date joined it on 2026-08-04 with
 * /requirement/, which is the first page to ask for one. Add the type here
 * rather than styling it on the page. */
input[type="text"], input[type="email"], input[type="tel"], input[type="url"],
input[type="date"],
select, textarea {
  width: 100%;
  max-width: 34rem;
  padding: var(--space-xs) var(--space-s);
  border: var(--rule-hair) solid var(--rule-ink-load);
  border-radius: var(--radius);
  background: var(--block);   /* white panel on muted, one lightness step on dark */
  color: var(--fg-hi);
}
input:hover, select:hover, textarea:hover { border-color: var(--fg-hi); }
input::placeholder, textarea::placeholder { color: var(--fg-lo); opacity: 1; }

.field .error {
  font-size: var(--step--1);
  color: var(--link);
  margin-block-start: var(--space-2xs);
}
/* Error state: doubled border weight plus the worded message. The
 * border stays in the neutral family so the page's orange-rule
 * ration is not spent on form states; colour is never the only
 * channel here because the message carries the word "Error". */
.field--error input, .field--error select, .field--error textarea {
  border-color: var(--fg-hi);
  border-width: 2px;
}

fieldset { border: var(--rule-hair) solid var(--rule-ink); padding: var(--space-l); }
legend { padding-inline: var(--space-2xs); font-weight: 500; color: var(--fg-hi); }

/* ================= 16. FIGURES ================= */

figure { margin: 0; }
/* Sentence case: captions can run to sentences, and multi-sentence
 * caps are banned. The mono face alone carries the spec voice. */
figcaption {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--fg-lo);
  /* Captions were uncapped and ran past 110 characters. The rule above
   * them is structural though: it is the edge the plate and its dot
   * field terminate against, so the BOX keeps the figure's width and
   * only the text is held to the measure. Padding does that without a
   * wrapper element, and clamps itself to 0 on a narrow figure. */
  padding-inline-end: max(0px, calc(100% - var(--measure)));
  padding-block-start: var(--space-xs);
  border-top: var(--rule-hair) solid var(--rule-ink);
  margin-block-start: var(--space-s);
}

/* ================= 17. DEMAND HERO ================= */
/* The Home mechanism. 8/4, never 6/6. The 4-column side carries
 * texture and the category definition, never a second audience.
 * Maximum four text elements: category label, headline, subtext, CTA. */

/* The hero owns the first screen, so it owns the whole first screen's
 * rhythm: a wrapping section never adds to it. Section padding (--space-4xl)
 * stacked on hero padding (--space-2xl) put ~216px of dead air above the
 * kicker at 1440x900 and pushed the CTA off the fold. */
section:has(.hero) { padding-block: 0; }
/* The band is the positioning context for the full bleed plate behind it, and
 * the type layer sits above it. Nothing else about the hero changes. */
.hero-band { position: relative; }
.hero-band .editorial { position: relative; z-index: 1; }

.hero {
  /* 85vh of the screen, but never more than the screen minus the nav.
   * The second term is what keeps the CTA above the fold on a laptop:
   * 85vh + the nav + the padding cannot fit in 900px, and the CTA is
   * what falls off. */
  min-height: min(85vh, calc(100vh - var(--shell-h)));
  /* The negative space is not deleted, it is distributed: align-content
   * splits the slack above AND below the hero instead of banking it all
   * on top. */
  align-content: center;
  /* The h1 owns its subline; the kicker above it is a separate idea.
   * Before this the loudest element on the site had equal clearance on
   * both sides, which is what made every hero read as a stack of four
   * unrelated lines instead of one statement with a part number over it. */
  row-gap: var(--gap-under-display);
  padding-block: var(--space-xl);
}
/* --space-2xs, not --space-2xl. The row-gap ALREADY separates the kicker from
   the h1; the margin was stacking a second interval on top of it and putting
   ~110px under the eyebrow on the homepage against ~18px on a module page.
   Chip, 2026-07-29: the eyebrow distance is the same everywhere, and the
   module pages are the reference. */
.hero .hero-title { margin-block-start: var(--space-2xs); }
.hero-head  { grid-column: col-start 1 / span 8; align-self: end; }
.hero-title { grid-column: col-start 1 / span 8; }
.hero-sub   { grid-column: col-start 1 / span 5; }   /* the 5-col lede against the 12-col measure */
.hero-cta   { grid-column: col-start 1 / span 8; }
.hero-aside {
  grid-column: col-start 9 / main-end;
  grid-row: 1 / 5;
  display: grid;
  grid-template-rows: 1fr auto;
  border-left: 2px solid var(--chrome);   /* the edge the dot field terminates against */
  padding-inline-start: var(--space-m);
}
.hero-aside .dots { min-height: 44vh; }
.hero-category {
  padding-block-start: var(--space-l);
  text-align: right;
  max-width: none;
  line-height: 1.7;
}
@media (max-width: 60rem) {
  .hero { min-height: 0; }
  .hero-head, .hero-title, .hero-sub, .hero-cta { grid-column: main; }
  /* The accent edge flips axis when the aside stacks, matching the
   * industry band's existing precedent. A left border on a full-width
   * stacked block is an orphan rule against nothing. */
  .hero-aside {
    grid-column: main;
    grid-row: auto;
    border-left: 0;
    border-block-start: 2px solid var(--chrome);
    padding-inline-start: 0;
    padding-block-start: var(--space-m);
  }
  .hero-aside .dots { min-height: 22vh; }
  .hero-category { text-align: left; }
}

/* Supply band: full bleed, after the hero, never inside it, kept
 * under 22vh, outlined CTA only. The fill is spent on the demand CTA. */
.supply-band { padding-block: var(--space-xl); }
.supply-band .band-inner {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-l);
  justify-content: space-between;
}

/* Section header: heading first in the DOM, the reference label at
 * the right edge. A .label is never the previous sibling of a heading. */
.sec-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-l);
  border-top: var(--rule-hair) solid var(--rule-ink);
  padding-block-start: var(--space-s);
  /* The heading rule: see 6b. The interval below lives there, once. */
}
/* The reference label is one token of data and never wraps. As a flex
 * item it was shrinking and breaking "01 / THE POSITION" across two
 * ragged lines at desktop. The h2 side already flexes. */
.sec-head .label { white-space: nowrap; flex-shrink: 0; }

/* ================= 18. FOCUS ================= */
/* A blue ring with a soft halo, on Chip's direction 2026-08-17 (this
 * replaces the GOV.UK yellow-and-bar composite the static site used).
 * Two parts on purpose: on the ROYAL/navy fills a bare blue outline
 * sinks into the ground, and the wider translucent halo is what keeps
 * the focused element findable there. Keyboard users still get a ring
 * on every surface; it is quieter, not weaker. */

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  box-shadow: 0 0 0 5px var(--focus-halo);
}

/* ================= 19. MOTION ================= */
/* The entire budget: three properties, one duration, one easing. */

.btn, a, input, select, textarea, summary, .nav-mega-btn {
  transition: border-color var(--t) linear, background-color var(--t) linear, color var(--t) linear;
}

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

/* ================= 20. FIGURES: THE LINE SYSTEM ================= */
/* See _reference/FIGURE-SYSTEM.md. Monochrome line art, one ink from
 * the surface, exactly as the logo inherits. Fills exist only on dots. */

.fig svg   { display: block; width: 100%; height: auto; color: var(--fg-hi); }
.fig-solid { fill: none; stroke: currentColor; stroke-width: calc(var(--rule-hair) * 2); }
/* 1.5x the hairline on every surface: at --rule-hair the dashes render
 * sub-perceptual on light grounds at 2dppx. Still two weights, still tied
 * to the token, and the solid weight is unchanged so the hierarchy holds.
 * Amends FIGURE-SYSTEM section 1.2. */
.fig-dash  { fill: none; stroke: currentColor;
             stroke-width: calc(var(--rule-hair) * 1.5);
             stroke-dasharray: var(--fig-dash); }
.fig-dot   { fill: currentColor; stroke: none; }
.fig-label {
  fill: currentColor;
  font-family: var(--font-mono);
  font-size: 22px;                 /* viewBox units: tracks the plate, small by design */
  letter-spacing: var(--track-label);
  text-transform: uppercase;       /* sentence case in the DOM, always */
}

/* Reveal. JS arms, the observer plays, CSS defaults to the finished
 * plate. With JS off no class is ever added and the figure is simply
 * complete. See §3 of the figure spec for the amended motion budget. */
.fig-armed .fig-solid { stroke-dasharray: 1; stroke-dashoffset: 1; }   /* pathLength="1" */
.fig-armed .fig-dash,
.fig-armed .fig-dot,
.fig-armed .fig-label { opacity: 0; }
.fig-played .fig-solid { stroke-dashoffset: 0;
  transition: stroke-dashoffset var(--fig-t) linear; }
.fig-played .fig-dash  { opacity: 1;
  transition: opacity var(--fig-t-fade) linear var(--fig-t); }
.fig-played .fig-label { opacity: 1;
  transition: opacity var(--fig-t-fade) linear calc(var(--fig-t) + 200ms); }
.fig-played .fig-dot   { opacity: 1;
  transition: opacity var(--fig-t-fade) linear calc(var(--fig-t) + 400ms); }

/* ---- 20b. The hero globe plate ----
 * assets/fig/fig-globe-converging-network.svg plus _system/globe.js, and the
 * notes file beside the SVG. A member of the family above, not a second
 * language: same viewBox, same two stroke weights, same dash token, same dot
 * radius, and it reuses .fig-solid, .fig-dash and .fig-dot untouched.
 *
 * Four things are its own, listed here so the difference is auditable rather
 * than discovered:
 *
 * 1. TONE. A numbered plate inks at --fg-hi. This one sits under hero type,
 *    where 17:1 would fight the headline, so the cage takes the load-bearing
 *    rule tone instead. It is a surface token, so the plate resolves on dark,
 *    light and muted with no variant class and no knowledge of which one it is
 *    on. Hero type crossing the busiest part of the plate measures 5.70:1 on
 *    dark and 4.79:1 on light and muted.
 * 2. ACCENT. FIGURE-SYSTEM 1.5 bans the accent inside a figure. This plate
 *    carries it, on the routes and their endpoint dots and nowhere else,
 *    because convergence is the thing the plate is for. Reported as a
 *    collision, not resolved silently: see the notes file.
 * 3. A FILL. FIGURE-SYSTEM 1.5 allows a fill only on a dot. Land is stroked in
 *    the cage tone and filled with the page ground, which is what makes the
 *    sphere read as solid rather than as a wire ball with shapes floating in
 *    it: the fill occludes the graticule behind the land. The fill is a surface
 *    token, so it is still the page showing through, not a colour.
 * 4. MOTION. A slow turn, which --t does not cover, driven by globe.js
 *    rewriting path data. See the notes file for why this figure earns a script
 *    when no other one does, and how the no-JS plate stays correct.
 *
 * It never carries data-fig: the reveal above is for a figure you scroll to,
 * and this one opens the page. */

.fig-globe      { color: var(--rule-ink-load); }
.fig-globe svg  { display: block; width: 100%; height: auto; }
/* The route is the subject of the plate, so it takes the heavier of the
 * system's two weights, not a third one, and keeps the dash pattern that says
 * it is a route. At the hairline the accent dashes read as scattered specks
 * against a graticule drawn in the same pattern. */
.fig-globe-link { stroke: var(--accent); stroke-width: calc(var(--rule-hair) * 2); }
/* The two kinds of point, and the whole idea of the deal cycle: the one with
 * the problem is a FILLED accent dot, the ones that could solve it are UNFILLED
 * accent circles. Filled against unfilled is the strongest distinction the line
 * system has, and it reads at hero size without leaning in.
 *
 * It is also how the accent stays inside its ration. A ring of hairline is a
 * fraction of the ink of a solid disc, so the providers stay quieter than the
 * holder by construction rather than by dimming, and a site that is not part of
 * a live deal is not drawn at all. The plate at rest carries a handful of marks,
 * never a field of them. Opacity on these elements is owned by the projector,
 * so it is never set here. */
.fig-globe-node { fill: var(--accent); }
.fig-globe-site { fill: none; stroke: var(--accent); stroke-width: calc(var(--rule-hair) * 2); }
/* The graticule sits behind the land at the same tone, one step quieter, so the
 * plate gains presence without gaining contrast: raising the whole cage to the
 * next token up would drop hero type over it from 5.70:1 to 3.40:1, which
 * passes only for large text. Separating the two layers is the legible win that
 * costs nothing. Static, never transitioned. */
.fig-globe-grat { opacity: 0.5; }
.fig-globe-land {
  fill: var(--bg);
  stroke: currentColor;
  stroke-width: calc(var(--rule-hair) * 2);
  stroke-linejoin: round;
}

/* Rotation is JS, so reduced motion is handled in globe.js as well as here.
 * This rule is the belt: it stops any transition the plate could pick up, and
 * globe.js checks the same query and puts the plate back to its resting frame,
 * which is the frame the markup shipped. Section 19 backstops both. */
@media (prefers-reduced-motion: reduce) {
  .fig-globe svg * { transition: none !important; animation: none !important; }
}

/* At 375 the whole 1600-unit plate scales down to a 124px globe, which is a
 * pea, not a hero. The wrapper crops the plate's empty left half instead and
 * the globe comes back up to roughly 273px across. No geometry changes: it is
 * the same plate, cropped, which is what the frame edge already does to Figs
 * 4, 6, 8 and 9. Nothing else in this file styles .fig-globe, so this query
 * does not depend on where it sits. */
/* The unmodified plate, in a full width band. Both modifiers opt out: their
 * own narrow-width rules are LESS specific than this selector, so without the
 * second :not this rule wins on specificity regardless of order and stacks its
 * margin on top of theirs. That put the mobile plate 400px off screen, and it
 * only surfaced when the marks were measured rather than looked at. */
@media (max-width: 60rem) {
  .fig-globe:not(.fig-globe--crop):not(.fig-globe--bleed) { overflow: hidden; }
  /* max-width: none is load-bearing, not tidying: the reset on line 40 clamps
   * every svg to 100% and silently ate the 220% until it was measured. */
  .fig-globe:not(.fig-globe--crop):not(.fig-globe--bleed) svg {
    width: 220%; max-width: none; margin-inline-start: -106%;
  }
}

/* The bleed, for a plate that IS the background of a band rather than a thing
 * inside it.
 *
 * FRAME THE POPULATED FACE, NOT THE SPHERE'S OUTLINE. Every touch point and
 * every route sits in the north Atlantic and Europe, which projects to the
 * upper right of the disc: measured over a full cycle the accent runs x 951 to
 * 1390 of the globe's 865 to 1395, and y 97 to 358 of its 85 to 615. So the
 * frame is solved around THAT box, and what gets sacrificed to the bleed is the
 * empty part: the western ocean, which goes behind the type, and the southern
 * hemisphere, which runs off the foot of the band. An earlier pass framed the
 * sphere's outline instead and kept the left limb and the empty Atlantic in
 * view while the whole subject sat off the right edge.
 *
 * The numbers are solved, not guessed. The plate is 1600 units wide, the globe
 * occupies 530 of them centred on 1130:
 *     diameter = 0.46 of the band width  ->  width = 1600 * 0.46 / 530 = 138.9%
 *     offset   = -11%, which is the furthest LEFT the plate can go while the
 *                leftmost accent mark of the whole cycle still lands clear of
 *                the h1's eight column box at every width from 960 up
 * Vertically the svg is centred and then lifted 37 percent of its own height,
 * which puts the populated northern band across the middle of the frame and
 * runs the empty south off the bottom.
 *
 * THE COMPOSITION IS THE ACCESSIBILITY. Hero type over the cage measures
 * 5.70:1 on dark, which is fine and is why the globe's empty western limb is
 * allowed behind the headline. Type over an accent route is 2.99:1, which
 * fails, so no accent mark may reach the type at any moment in the cycle. The
 * build script asserts exactly that against these two numbers and fails loudly
 * if a change to the deal cycle or the framing breaks it. Do not edit the two
 * values below without re-running it. */
.fig-globe--bleed {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}
.fig-globe--bleed svg {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: -11%;
  width: 138.9%;
  max-width: none;
  transform: translateY(-37%);
}
/* The band stacks at narrow widths: type first, then the plate under it at a
 * size worth looking at. Behind full width type the routes would cross the
 * lede, and that is the 2.99:1 case.
 *
 * position: relative is load-bearing, not tidying. The svg inside is absolutely
 * positioned; with the wrapper static it resolved against the band instead and
 * threw the whole plate several hundred pixels off screen, which only showed up
 * when the marks were measured rather than looked at.
 *
 * The framing also changes here, and it should: at this width the whole sphere
 * fits, so there is nothing to sacrifice and the frame goes back to the
 * outline. Same derivation as the desktop one, solved for the globe filling
 * 95 percent of the box:
 *     width  = 1600 * 0.95 / 530           = 286.8%
 *     offset = 0.5 - (1130/1600) * 2.868   = -152.6% */
@media (max-width: 60rem) {
  .hero-band { display: flex; flex-direction: column; }
  .fig-globe--bleed {
    position: relative;
    order: 2;
    aspect-ratio: 1 / 1;
    margin-block-start: var(--space-xl);
  }
  .fig-globe--bleed svg {
    inset-inline-start: -152.6%;
    width: 286.8%;
    transform: translateY(-50%);
  }
}

/* The crop, for a plate in a column rather than across a band. The plate is
 * 1600 units wide and the globe occupies 530 of them centred on 1130, so the
 * empty left half is cropped away and the globe is sized to the box instead of
 * the frame. At 286.8% the globe comes to 95 percent of the box width, and
 * -152.6% puts its centre on the box centre:
 *     width  = 0.95 * 1600 / 530          = 286.8%
 *     offset = 0.5 - (1130 / 1600) * 2.868 = -152.6%
 * Vertical centring is free: the globe sits at half the viewBox height, so
 * centring the svg centres the globe. Same at every width, so the narrow-width
 * crop above steps aside for it. */
.fig-globe--crop { position: relative; overflow: hidden; }
.fig-globe--crop svg {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: -152.6%;
  width: 286.8%;
  max-width: none;
  transform: translateY(-50%);
}

/* ================= 21. THE INDUSTRY HEADER ================= */
/* The four industry verticals ONLY. They were the site's one major
 * family opening on an unauthored wall of type while their photograph
 * sat buried mid-page, so the photograph moves up and becomes the
 * opening. Type left, photograph right, one accent rule between them.
 *
 * The band is simultaneously the page's ONE grid break and its ONE
 * surface change, which is why the mid-page photo section is removed
 * from those four pages: the image moves, nothing is duplicated.
 *
 * Nobody else takes this. The hub and home pages have the dot-field
 * hero and it works. Two authored hero patterns is a system; three is
 * a collection. See POLISH-SPEC section 3. */

.hero-vert { padding-block: 0; }   /* the band's edges are the photograph's edges */
.hero-vert .editorial {
  /* 72vh, never 100: the band has to visibly END above the fold or the
   * reader has no reason to believe the page continues. */
  min-height: 72vh;
  row-gap: 0;
}

.hero-vert-copy {
  grid-column: main-start / span 7;
  grid-row: 1;
  align-self: center;
  /* Not --space-3xl. A step-6 headline in a 7-column track runs to four
   * lines, and 3xl on top of that pushed the band past the full viewport,
   * which is the one thing min-height: 72vh exists to prevent. */
  padding-block: var(--space-xl);
  display: grid;
  justify-items: start;
  row-gap: var(--gap-under-heading);
}
/* Same law as the demand hero: the h1 owns its lede, the label above is
 * a separate idea and gets the wider interval. */
.hero-vert-copy h1 { margin-block-start: var(--space-xl); }

/* Bleeds to the right viewport edge and to the band's top and bottom.
 * No caption, no pill, no text on it, ever. */
.hero-vert-media {
  grid-column: col-start 8 / full-end;
  grid-row: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* One of the page's two permitted orange rules, and the same device as
 * the home hero's terminator: an edge the composition stops against. */
.hero-vert-rule { border-inline-start: 2px solid var(--chrome); }

/* Focal points, POLISH-SPEC section 5. A header image is cropped tall at
 * desktop, so each frame names the subject that must survive the crop.
 *
 * THESE FOUR VALUES WERE DERIVED FOR A CROP THAT NO LONGER EXISTS, and they
 * are kept anyway. Read this before re-deriving any of them.
 *
 * They were authored against .hero-vert-media: a TALL, NARROW box, five of
 * twelve columns wide and the full height of the band. On 2026-08-03 the four
 * sector pages that carry a photograph moved to the full-bleed hero in section
 * 45, where the same frames are cropped WIDE and SHORT instead: the full page
 * width against roughly 36rem of height. A crop that is wide where it used to
 * be narrow keeps far more of the horizontal frame and far less of the
 * vertical, so a horizontal percentage that used to pull a subject into view
 * may now be doing nothing, and a vertical one that used to be safe may now be
 * cutting.
 *
 * WHAT EACH ONE WAS PROTECTING, so the intent survives the re-derive:
 *   machinist  the operator sits centre-right of frame.
 *   long-lead  the small figure at FAR LEFT. That figure is the entire scale
 *              argument of the energy page. A crop that loses it breaks the
 *              image, and this is the one value that must never be relaxed
 *              toward centre without checking the left edge first.
 *   gear-face  the open planetary gear face, centred.
 *   rotor      crops the tool-strewn floor out of the bottom.
 *
 * NOT RETUNED NOW, deliberately. New commissioned photographs are being shot
 * for these four slots and every number here is a property of the frame, not
 * of the layout. Re-deriving against images that are about to be replaced is
 * work thrown away twice. All four remain legal object-position values and all
 * four still apply, so nothing renders wrong in the meantime; they are simply
 * no longer known to be optimal. RE-DERIVE ALL FOUR AGAINST THE WIDE CROP THE
 * DAY THE NEW FILES LAND, using the subjects listed above as the test.
 *
 * STILL LIVE ON THE OLD COMPONENT TOO: .hero-vert-media has not been deleted
 * and these rules are not scoped to either hero, so they govern whichever
 * component the class is written on. */
.focal--machinist { object-position: 65% 45%; }
.focal--long-lead { object-position: 30% 50%; }  /* the small figure at far left IS the scale argument */
.focal--gear-face { object-position: 40% 50%; }
.focal--rotor     { object-position: 50% 35%; }  /* crops the tool-strewn floor out */

/* ================= 22. SPACING UTILITIES ================= */
/* The only sanctioned replacement for a style attribute on a page.
 * Every value is a token; the names map one to one onto the space scale
 * so promoting an inline style is a swap, never a judgement call.
 * These exist because inline styles are where the next drift starts. */

.push-xs    { margin-block-start: var(--space-xs); }
.push-s     { margin-block-start: var(--space-s); }
.push-m     { margin-block-start: var(--space-m); }
.push-l     { margin-block-start: var(--space-l); }
.push-xl    { margin-block-start: var(--space-xl); }
.push-end-l { margin-block-end: var(--space-l); }

/* A dot field used as a spacer band between sections needs a height,
 * and the two heights already in use are the whole requirement. */
.dots--band      { min-height: var(--space-2xl); }
.dots--band-tall { min-height: var(--space-3xl); }


/* Module rows carry their own top hairline, which breaks into fragments
 * with gutter gaps across a multi-column grid and is most of why that page
 * reads as a card grid it is not. Opt out here and run one full-width
 * .rule above the row-group instead. */
.modules--flush > .module { border-top: 0; }

/* The homepage module showcase carries name plus tag and no description,
 * so the subgrid is two rows rather than three. Without this the empty
 * third row still spends a row-gap and every tag sits on a stray
 * interval above its own rule. */
.modules--names > .module { grid-row: span 1; }  /* name only. Was 2 with the tag. */

/* Each showcase group is a <section> so its label and its framing line
 * reach the accessibility tree through aria-labelledby and
 * aria-describedby. Structural only: the element default would stack
 * --space-3xl of section padding on every group. Spacing between groups
 * is the existing .push-xl utility, not a number invented here. */
.module-group { padding-block: 0; }

/* ================= 23. NARROW WIDTHS ================= */
/* Everything above reads as generous at desktop and as abandoned at
 * 375px: open padding plus row-gap plus section padding stacked to
 * roughly 500px of unbroken empty ground between a caption and the next
 * heading. This query is the whole fix, and it is the LAST query in the
 * file on purpose, so it wins against the declarations it narrows. */

@media (max-width: 60rem) {
  /* The section ladder collapses in 6b, in proportion, so the order of
   * open / default / dense survives the breakpoint. */
  .editorial     { --editorial-row-gap: var(--space-l); }
  .shell-footer  { margin-top: var(--space-2xl); }

  /* A 48px headline in a 256px column eventually meets a word it cannot
   * fit: "introductions" is 318px, "Qualification" 300px, and seven hero
   * pages scrolled sideways at 320 because of it. Hyphenate rather than
   * lower the hero floor, so the scale survives the small screen. Every
   * page declares lang="en-CA", so these are real hyphenation points.
   * anywhere, not break-word, is the net under it: break-word leaves the
   * min-content width at the whole word, and the industry band sizes its
   * copy column with justify-items: start, so the BOX still overflowed
   * even though the text could have broken. Emergency breaks are the
   * last resort the line breaker reaches for, after hyphenation. */
  h1, .display, .pull p:first-child, .lede-text {
    hyphens: auto;
    overflow-wrap: anywhere;
  }

  /* The desktop fix for ragged section labels (nowrap + no shrink) pushed
   * the longer ones past the right edge here and scrolled the document
   * sideways. On a narrow screen the label takes its own row instead,
   * where it has the full measure and almost never needs to wrap at all. */
  .sec-head { flex-wrap: wrap; }
  .sec-head .label { white-space: normal; }

  /* The band goes single column: label, headline, lede, then the image
   * full width at its native 16:10 with the accent rule above it. */
  .hero-vert .editorial { min-height: 0; row-gap: var(--space-l); }
  .hero-vert-copy {
    grid-column: main;
    grid-row: auto;
    padding-block: var(--space-2xl) 0;
  }
  .hero-vert-media {
    grid-column: full;
    grid-row: auto;
    height: auto;
    min-height: 200px;     /* below this the subject stops reading at all */
    aspect-ratio: 16 / 10;
  }
  .hero-vert-rule {
    border-inline-start: 0;
    border-block-start: 2px solid var(--chrome);
  }
}

/* The shop pages' sanctioned accent edge (POLISH-SPEC section 6.2, FIGURE-SYSTEM
 * section 5.2). One of the two orange rules those pages are permitted, and the mark
 * that makes the price table read as the page's centre of gravity. Structural, not
 * decorative: it is an edge the table stops against, the same device as the home
 * hero terminator and the industry header rule. */
.edge-accent { border-inline-start: 2px solid var(--chrome); padding-inline-start: var(--space-m); }

/* ================= 24. VISUALLY HIDDEN ================= */
/* The only way to give a region a name a screen reader can use without
 * printing that name on a page whose design does not want it. Not
 * display:none, which removes it from the accessibility tree too. */

.vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* ================= 25. THE LOGO BAR ================= */
/* Third-party marks under the hero. COMPANY-SITE-SPEC 6.1 governs the
 * treatment; index.html carries the per-mark provenance and the list of
 * what may never appear here.
 *
 * ONE COLOUR, ONE OPTICAL SIZE. Every mark is painted with --fg-hi
 * through a mask, so the file supplies the SHAPE and the surface
 * supplies the ink. That is why this works on all three grounds and why
 * a raster mark is treated exactly like a vector one. Nothing is
 * recoloured to the accent, nothing is cropped, no aspect ratio is
 * touched: mask-size is contain, always.
 *
 * ONE OPTICAL SIZE MEANS ONE CAP HEIGHT, NOT ONE BOX HEIGHT. Equal
 * boxes make a mark with tall clearance read smaller than a plain
 * wordmark: Boeing's swoosh, Lockheed's star and the Method Effect's arc
 * all overhang their letterforms by different amounts. So --mark-cap is
 * the single authored size for the whole row and every box is derived
 * from it. --mark-cap-frac is not a design value, it is a MEASUREMENT of
 * the file: the cap height (x-height for the one lowercase mark) as a
 * fraction of its own box, taken off the alpha channel in the browser at
 * a 400px render. Same for --mark-ratio, which is the file's width over
 * its height and is what keeps the artwork from ever being stretched.
 * Re-measure, do not guess, if a file is replaced. */

/* CENTRED AS A ROW, not justified to the edges. Chip, 2026-07-29.
   It span-to-the-edges'd first, which pinned the first and last marks to the
   container and let the gaps fall wherever five unequal widths left them: the
   row read as stretched rather than as a set. Centre plus ONE even gap means
   the space between any two marks is the same number, which is what makes a
   logo row look deliberate.
   align-items: center is the vertical half of the same idea, and the marks are
   already cap-aligned to each other by --mark-cap-frac, so they sit on one
   optical line rather than one box line. */
.proofbar {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  /* --space-2xl, not --space-3xl. ARITHMETIC, not taste: five marks at a 12px
     cap measure 814px together and the container is 1108, which leaves 294 for
     four gaps, so 73px each is the ceiling. At --space-3xl (108) the fifth mark
     wrapped to its own line and took the bar under the fold with it.
     Adding a sixth mark means re-running that sum. */
  gap: var(--space-m) var(--space-2xl);
}
@media (max-width: 40rem) {
  /* One even gap of --space-3xl between five marks does not fit a phone. They
     keep the centring and take the smaller gap. */
  .proofbar { gap: var(--space-m) var(--space-l); }
}

.mark {
  /* 12px. 13 -> 16 at four marks, 14 when Doyletech made it five, now 12
     (Chip, "slightly smaller and more spaced out"): the size came down so the
     even --space-3xl gap has room to sit between five marks on one row.
     This is the ONE number that sizes the row: every mark scales from its own
     measured cap fraction, so they sit on a common CAP HEIGHT rather than a
     common box. Adding a sixth mark will need this number revisited again. */
  --mark-cap: 0.75rem;
  --mark-h: calc(var(--mark-cap) / var(--mark-cap-frac));
  display: block;
  width: calc(var(--mark-h) * var(--mark-ratio));
  height: var(--mark-h);
  background-color: var(--fg-hi);
  -webkit-mask: var(--mark-src) no-repeat center / contain;
          mask: var(--mark-src) no-repeat center / contain;
}

.mark--boeing {
  --mark-src: url("logo/boeing.svg");
  --mark-ratio: 4.365; --mark-cap-frac: 0.362;
}
.mark--lockheed {
  --mark-src: url("logo/lockheed-martin.svg");
  --mark-ratio: 6.620; --mark-cap-frac: 0.378;
}
.mark--vizworx {
  --mark-src: url("logo/vizworx.png");
  --mark-ratio: 4.717; --mark-cap-frac: 0.445;
}
/* Doyletech Corporation. Added 2026-07-29 (Chip). Source: the company's own
 * site, doyletechcorp.com/wp-content/uploads/2023/10/dtlogo-in-black-800px.png.
 * The file ships as RGB on white with no alpha, and this bar masks on the
 * ALPHA channel, so a straight drop-in would have rendered a solid rectangle.
 * The alpha was rebuilt from luminance and the transparent padding cropped.
 * The cap fraction is the OYLETECH capitals only (rows 100 to 162 of 234),
 * NOT the swoosh, which runs the full height, and NOT the CORPORATION line
 * under it. Measured off the alpha, per the note at the top of this block. */
.mark--doyletech {
  --mark-src: url("logo/doyletech.png");
  --mark-ratio: 3.415; --mark-cap-frac: 0.269;
}
/* The one lowercase wordmark, so the fraction is its ascender height and
 * not a cap height. Matching ascender to cap is what makes it sit level
 * with the four sets of capitals beside it. */
.mark--method {
  --mark-src: url("logo/the-method-effect.png");
  --mark-ratio: 4.268; --mark-cap-frac: 0.285;
}

/* ================= 26. THE QUOTE CAROUSEL ================= */
/* PROGRESSIVE ENHANCEMENT, AND THE ORDER MATTERS.
 * Everything above the .is-live block is the no-JavaScript component: a
 * wrapped grid holding every quote, all of them readable, nothing
 * clipped and no scroll of any kind. That is the component. The .is-live
 * rules below only exist once _system/quotes.js has confirmed it can
 * drive the thing. Never move a base rule into .is-live. */

.quotes-note {
  font-size: var(--step--1);
  color: var(--fg-lo);
  max-width: var(--measure);
  margin-block-end: var(--space-xl);
}

.quote-track {
  list-style: none;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--space-xl) var(--space-m);
}

/* Same idiom as .module: a hairline above, no card, no radius, no
 * shadow, no quotation-mark ornament (.pull's rule, applied here). */
.quote {
  min-width: 0;                 /* a long unbroken string cannot widen the row */
  border-top: var(--rule-hair) solid var(--rule-ink);
  padding-block-start: var(--space-m);
}
.quote blockquote p {
  color: var(--fg-hi);
  font-size: var(--step-0);
  text-wrap: pretty;
}
/* The global figcaption is the FIGURE caption: mono, with its own
 * hairline above it. Both are wrong here. This is a person's name, not
 * spec metadata, and a second rule inside a card that already opens on
 * one turns three quotes into six lines. Reset to the body face and let
 * the card's own rule be the only one. */
.quote figcaption {
  margin-block-start: var(--space-m);
  font-family: var(--font-body);
  font-size: var(--step--1);
  color: var(--fg-lo);
  border-top: 0;
  padding-block-start: 0;
  padding-inline-end: 0;
}

/* A band paints its own ground, so the ground has to close below its
 * last line. Same rule and same reasoning as the one on
 * main > [data-surface]:last-child in section 6b; this component is the
 * one that ends a band without being the last band on the page, and
 * without this its buttons sit flush on the colour change. */
[data-surface] > .quotes:last-child { padding-block-end: var(--space-3xl); }
@media (max-width: 60rem) {
  [data-surface] > .quotes:last-child { padding-block-end: var(--space-2xl); }
}

/* ---- the enhanced state ---- */
/* One page is exactly the visible width, because three basis widths plus
 * two gaps resolve to 100%. That is what lets the script advance by
 * clientWidth and land on a card edge without measuring anything. */
.quotes.is-live .quote-track {
  display: flex;
  gap: 0 var(--space-m);
  overflow-x: auto;
  overscroll-behavior-x: contain;   /* a swipe at the end never turns into a page navigation */
  scroll-snap-type: x proximity;    /* proximity, not mandatory: mandatory can strand a final
                                     * partial page that has no snap point inside the range */
  scrollbar-width: none;            /* the buttons are the affordance, and the row is a scroll
                                     * container the keyboard can still reach and drive */
}
.quotes.is-live .quote-track::-webkit-scrollbar { display: none; }

.quotes.is-live .quote {
  flex: 0 0 100%;
  scroll-snap-align: start;
}
@media (min-width: 48rem) {
  .quotes.is-live .quote { flex-basis: calc((100% - var(--space-m)) / 2); }
}
@media (min-width: 64rem) {
  .quotes.is-live .quote { flex-basis: calc((100% - 2 * var(--space-m)) / 3); }
}

.quote-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s);
  margin-block-start: var(--space-xl);
}

/* ==========================================================================
   THE FOOTER QUOTES AND THE CTA BAR. Chip, 2026-07-29.
   Appended at the END of this file on purpose. Earlier nav rules were written
   mid-file, above the generic .mega .link-index rules they needed to beat,
   tied on specificity and silently lost. Anything added from here on goes at
   the bottom where source order is on its side.
   ========================================================================== */

/* The quotes declare NO ground. They inherit the page's, which is what makes
   them match the section above them on every page instead of on one. */
.footer-quotes { padding-block: var(--space-3xl) 0; }
.footer-quotes > .wrap > .label { display: block; margin-block-end: var(--space-l); }
/* .quote is reused verbatim from the homepage module: hairline above, no card,
   no radius, no ornament. Only the layout is new. auto-fit so the row collapses
   to two and then one without a breakpoint per count. */


/* ---- THE NAV LABEL BECOMING A LINK ----
   Chip, 2026-07-29. Appended at the end for source order, same reason as the
   block above. */

/* The label is now an <a> and the caret a sibling <button>, so the pair has to
   sit on one baseline and keep the single-control look the bar had. */
.nav-mega { display: inline-flex; align-items: baseline; }
.nav-mega-caret {
  background: none;
  border: 0;
  padding: 0 0 0 var(--space-3xs);
  font: inherit;
  color: inherit;
  cursor: pointer;
}

/* A page-level index. Same grid as the industries panel uses, at page level
   rather than in chrome. */
.page-index { margin-block-start: var(--space-m); }





/* ==========================================================================
   THE HERO BAND, WITH THE PROOF BAR INSIDE IT. Chip, 2026-07-29.
   The marks have to be above the fold, and the fade is what lets them sit
   over the globe plate without the accent routes coming up under a logo.
   ========================================================================== */

/* One screen, minus the header. The type takes the top, the marks take the
   bottom, and the plate runs behind both. min-height, never height: the type
   is allowed to make the band taller on a short window rather than clip. */
.hero-band {
  /* EXACTLY the rest of the first screen. Chip, 2026-07-29: the black has to
     read as a full 100vh on desktop, with the marks inside it rather than the
     next surface showing under them.
     --shell-total is the nav PLUS the notice above it, measured by shell.js
     because the notice self-removes after the Congress. --shell-h is the
     no-JS fallback: the nav alone, which is the right answer on the run where
     shell.js never executes and the notice is the hardcoded one. */
  /* min(), so a tall screen is not padded out with blank. 46rem is what the
     hero and the marks actually need; on a shorter window the second term wins
     and the band shrinks to the viewport instead of pushing the marks under
     the fold. Chip, 2026-07-29: "on taller screens make it so it's not 100VH,
     there is just a LOT of blank space". */
  min-height: min(46rem, calc(100svh - var(--shell-total, var(--shell-h))));
  display: flex;
  flex-direction: column;
}
/* The editorial keeps its NATURAL height. Stretching it to fill the band made
   the grid distribute 400px of free space above the h1 and pushed the marks
   below the fold, which is the opposite of the point. The bar is pinned to the
   foot by its own margin-block-start: auto instead. */
.hero-band > .editorial { flex: 0 0 auto; }

/* THE FADE. Sits above the plate and below the content, which is the whole
   trick: z-index 0 against the content's z-index 1, and the plate has none.
   It fades to --bg rather than to a hardcoded black so it still works if the
   hero is ever put on another surface. */
.hero-band::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  block-size: 42%;
  background: linear-gradient(to bottom, transparent, var(--bg) 72%);
  z-index: 0;
  pointer-events: none;
}

.proofbar-band {
  position: relative;
  z-index: 1;                       /* over the fade, same layer as the type */
  margin-block-start: auto;         /* pinned to the foot of the band */
  padding-block-start: var(--space-2xl);
  padding-block-end: var(--space-3xl);   /* the air Chip asked for under them */
}

@media (max-width: 60rem) {
  /* The band stacks on a phone and the plate becomes a real block in the
     flow, so there is nothing to fade over and no fold to beat. */
  .hero-band { min-height: 0; }
  .hero-band::after { content: none; }
  .proofbar-band { padding-block: var(--space-xl) var(--space-2xl); }
}


/* Hero: the type leads and the mark is a mark, not a column of its own. */
.mod-hero .editorial { row-gap: var(--space-m); padding-block: 0; }
/* 24ch, not 18. At 18ch two of the fifteen headlines wrapped to three lines
   while the other thirteen took two, which is the inconsistency the length
   bands exist to remove. The band is 40 to 48 characters; 24ch fits all of it
   in two lines. */
.mod-hero h1 { margin-block-start: var(--space-2xs); max-width: 24ch; }
.mod-hero .lede-text { max-width: 42ch; }
.mod-hero .hero-cta { margin-block-start: var(--space-s); }

.mod-faq .sec-head { margin-block-end: var(--space-l); }

/* The hero mark, sized to read as a deliberate plate rather than a stray
   glyph in the corner. It is the module's own mark from the index, drawn at
   the scale a figure gets. It is NOT a screenshot and the generated page says
   why; when real captures land it takes their place. */


@media (max-width: 60rem) {
}

/* ==========================================================================
   THE FOOTER. Rebuilt 2026-07-29 (Chip). ALWAYS DARK on every page, whatever
   the page above it is. Order: quotes, then the two doors, then the columns.
   The footer being one consistent object site-wide is what lets the doors use
   the same orange-and-white pair as the header panel without either surface
   fighting the page it happens to sit on.
   ========================================================================== */

.shell-footer { background: var(--bg); color: var(--fg); }

/* ---- QUOTES ---- outlined blocks on the same ground, so the outline does
   the separating rather than a fill. Larger than the old set, per Chip. */
.footer-quotes { padding-block: var(--space-3xl) var(--space-xl); }

/* A scroll-snap row. With scripts off this is still every quote, readable and
   scrollable; only the arrows go quiet. */
.footer-quote-grid::-webkit-scrollbar { display: none; }
/* The block, outlined rather than filled. .quote's own top hairline is
   replaced by a full outline here, so it reads as a card without a fill. */

/* ==========================================================================
   MODULE FEATURE BLOCKS. 2026-07-29, on the DOSS module shape.
   Copy left, screenshot right, same every time. The frames are empty on
   purpose and hold their aspect so nothing reflows when captures land.
   ========================================================================== */

.mod-hero { padding-block: var(--space-2xl) var(--space-2xl); }
.mod-hero .editorial { row-gap: var(--space-m); padding-block: 0; }
.mod-hero h1 { margin-block-start: var(--space-2xs); max-width: 20ch; }
/* The lede is long here by design, so it takes a reading measure rather than
   the short statement width the old one-line lede used. */
.mod-hero .lede-text { max-width: 54ch; }
.mod-hero .hero-cta { margin-block-start: var(--space-s); }

.mod-feature { padding-block: 0 var(--space-3xl); }
.mod-feature-inner {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: var(--space-2xl);
  align-items: start;
}
.mod-feature-copy h2 { max-width: 22ch; }
.mod-feature-desc {
  margin-block-start: var(--space-m);
  max-width: var(--measure);
  color: var(--fg);
}
/* Plain markers, not bolded-phrase bullets: COPY-DIRECTION bans the bolded
   label pattern, and a hairline row is the site's own device for a short list. */
.mod-feature-list { list-style: none; padding: 0; margin-block-start: var(--space-l); }
.mod-feature-list li {
  padding-block: var(--space-s);
  border-top: var(--rule-hair) solid var(--rule-ink);
  color: var(--fg-hi);
}
.mod-feature-list li:last-child { border-bottom: var(--rule-hair) solid var(--rule-ink); }

.mod-shot-frame {
  margin: 0;
  border: var(--rule-hair) solid var(--rule-ink-load);
  background: var(--block);
  aspect-ratio: 4 / 3;
  display: grid;
  place-content: center;
}
.mod-shot-frame figcaption {
  font-family: var(--font-mono); font-size: var(--step--2);
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--fg-lo); border: 0; padding: 0;
}
.mod-shot-frame:not(.mod-shot--empty) { aspect-ratio: auto; display: block; }
.mod-shot-frame img { display: block; width: 100%; height: auto; }

@media (max-width: 60rem) {
  .mod-feature-inner { grid-template-columns: 1fr; gap: var(--space-l); }
  .mod-shot-frame { aspect-ratio: 16 / 9; }
}



/* ---- THE HERO CLOSE ---- air, a rule, air. The features start as their own
   movement instead of running straight on from the CTA. */
.mod-rule { margin-block: var(--space-3xl); }

/* ---- FEATURE NUMBERING ---- The count varies by module, so a reader needs to
   know how far through a set they are. This is the one place a number is not a
   claim about anything. */
.mod-feature-n {
  font-family: var(--font-mono); font-size: var(--step--2);
  letter-spacing: var(--track-label); color: var(--fg-lo);
  margin-block-end: var(--space-s);
}

/* ==========================================================================
   COMMON QUESTIONS. Was a bare stack of hairlines with no section treatment,
   which read as unstyled next to the feature blocks above it.
   It gets the site's own .sec-head, a bounding rule, real row padding, and an
   affordance that changes on open.
   ========================================================================== */
.mod-faq { padding-block: var(--space-2xl) var(--space-3xl); }
.mod-faq .sec-head { margin-block-end: var(--space-l); }
.mod-faq-list {
  border-top: var(--rule-med) solid var(--rule-ink-load);
  max-width: 72ch;
}
.faq { border-top: 0; border-bottom: var(--rule-hair) solid var(--rule-ink); }
/* DIRECT CHILD, not descendant, throughout this block. #agenda nests a second
 * <details class="sess"> inside each day's <details class="faq">, and with a
 * descendant selector every rule below landed on those too: session titles
 * rendered at --step-2, took --space-l of vertical padding, and grew a second
 * plus marker. A session is a line in a timetable, not a day heading. The >
 * is the whole fix and it changes nothing for the day rows themselves. */
/* THE CLAIM ROW'S LEAD-IN IS A TITLE, so it is set as one. Twenty claim rows
 * across the site open with a bolded phrase and then a sentence, and both ran at
 * body size in the same face, so the row read as one undifferentiated block.
 * Scoped to a first-child strong inside a claim paragraph, which is exactly the
 * lead-in shape and nothing else. Verified: all 20 are first-child. */
.claim p > strong:first-child {
  display: block;
  font-size: var(--step-1);
  line-height: var(--lh-heading);
  margin-block-end: var(--space-2xs);
}

/* The question was the same face, weight and colour as its own answer, so an
 * open FAQ row read as one paragraph. Display face plus 600 separates them
 * without touching size, which the row's layout depends on. */
.faq > summary {
  font-family: var(--font-display);
  font-weight: 600;
  cursor: pointer; list-style: none;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--space-l);
  padding-block: var(--space-l);
  font-size: var(--step-1); line-height: var(--lh-heading);
  color: var(--fg-hi);
}
.faq > summary > span { max-width: var(--measure); }
.faq > summary::-webkit-details-marker { display: none; }
/* A text affordance rather than an icon, matching the nav caret. */
.faq > summary::after {
  content: "+"; flex: none; color: var(--fg-lo);
  font-size: var(--step-1); line-height: 1;
}
.faq[open] > summary::after { content: "\2212"; color: var(--fg-hi); }
.faq[open] > summary { padding-block-end: var(--space-s); }
.faq > summary:hover { color: var(--link); }
.faq > summary:hover::after { color: var(--link); }
.faq-a { padding-block-end: var(--space-l); }
.faq-a p { max-width: var(--measure); color: var(--fg); }

/* ==========================================================================
   PREVIOUS / NEXT. Was two bare rules with text under them. They are the only
   way on from the bottom of a module page now that the all-modules link is
   gone, so they read as destinations: a full-height block each, a rule that
   belongs to the block, and the module name at heading size.
   ========================================================================== */
.mod-nav { padding-block: 0 var(--space-4xl); }
.mod-nav-inner { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-l); }
.mod-nav-link {
  display: flex; flex-direction: column; gap: var(--space-2xs);
  padding: var(--space-l);
  border: var(--rule-hair) solid var(--rule-ink-load);
  text-decoration: none;
  min-height: 8rem;
  justify-content: center;
}
.mod-nav-link--next { text-align: right; align-items: flex-end; }
.mod-nav-link--next:only-child { grid-column: 2; }
.mod-nav-dir {
  font-family: var(--font-mono); font-size: var(--step--2);
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--fg-lo);
}
.mod-nav-name { font-size: var(--step-2); line-height: var(--lh-heading); color: var(--fg-hi); }
.mod-nav-link:hover { border-color: var(--fg-hi); }
.mod-nav-link:hover .mod-nav-name { color: var(--link); }
@media (max-width: 48rem) {
  .mod-nav-inner { grid-template-columns: 1fr; }
  .mod-nav-link--next { text-align: left; align-items: flex-start; }
  .mod-nav-link--next:only-child { grid-column: 1; }
}

/* ==========================================================================
   THE HOMEPAGE HERO BAND. 2026-07-29, after three wrong turns worth writing
   down so nobody repeats them.

   1. min-height CANNOT shrink anything. It is a floor. Capping the band with
      min-height: min(46rem, ...) did nothing, because the band was 990px tall
      from its own CONTENT, not from a floor.
   2. The content was 990 because `.hero` carries its own
      min-height: min(85vh, 100vh - shell-h), which floored the editorial at
      exactly the 765px of room above the fold and left the marks no space at
      all. The band has to hold BOTH, so the hero gives up that floor here.
   3. The eyebrow gap was never the h1 margin. The editorial grid's row-gap
      sits between the kicker and the h1 as well, so shrinking the margin moved
      nothing. The row-gap is the number that matters.
   ========================================================================== */

.hero-band > .editorial.hero {
  min-height: 0;                    /* the band owns the height, not the hero */
  padding-block: var(--space-m);
  row-gap: var(--space-m);          /* THIS is the eyebrow-to-h1 distance */
}
.hero-band .proofbar-band {
  padding-block-start: var(--space-l);
  padding-block-end: var(--space-l);
}

/* ==========================================================================
   FOOTER TESTIMONIALS. 2026-07-29, Chip's notes.
   A real title, square arrow buttons, taller blocks, and the attribution
   pinned to the bottom of each card with the name in the accent.
   ========================================================================== */

.fq-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--space-l); margin-block-end: var(--space-l);
}
.fq-title h2 { color: var(--fg-hi); margin-block-end: var(--space-2xs); }

/* SQUARE. Equal width and height, set from one value so they cannot drift. */
.fq-arrows { display: flex; gap: var(--space-2xs); margin: 0; flex: none; }
.fq-arrow {
  --fq-arrow-size: 3rem;
  inline-size: var(--fq-arrow-size);
  block-size: var(--fq-arrow-size);
  display: grid; place-content: center;
  background: none;
  border: var(--rule-hair) solid var(--rule-ink-load);
  color: var(--fg); font: inherit; line-height: 1; cursor: pointer;
}
.fq-arrow:hover { color: var(--fg-hi); border-color: var(--fg-hi); }
.fq-arrow:disabled { opacity: 0.35; cursor: default; }
.fq-arrow:disabled:hover { color: var(--fg); border-color: var(--rule-ink-load); }

/* Taller, and the attribution sits on the floor of the card so the names line
   up across the row however long the quotes run. */
.footer-quotes .quote {
  scroll-snap-align: start;
  border: var(--rule-hair) solid var(--rule-ink-load);
  padding: var(--space-l);
  min-block-size: 22rem;
}
.footer-quotes .quote figure {
  margin: 0; block-size: 100%;
  display: flex; flex-direction: column;
}
.footer-quotes .quote blockquote { margin: 0; }
.footer-quotes .quote blockquote p { color: var(--fg-hi); font-size: var(--step-0); }
.footer-quotes .quote figcaption {
  margin-block-start: auto;          /* pinned to the bottom of the card */
  padding-block-start: var(--space-l);
  border: 0;
  display: flex; flex-direction: column; gap: var(--space-3xs);
}
/* The name takes the accent, the employer stays quiet. Attribution is not
   endorsement: naming an employer here is permitted, rendering its mark is not. */
.q-who { color: var(--link); }
.q-org { color: var(--fg-lo); font-size: var(--step--1); }

/* ---- THE TESTIMONIAL TRACK ----
   THREE VISIBLE, THE FOURTH PEEKING. Chip, 2026-07-29: three is enough on
   screen, and the next one should show at the edge and fade so it is obvious
   there is more to advance to.
   The card width is derived, not typed: three cards and their two gaps fill
   the container exactly, and dividing by 3.35 instead of 3 leaves the peek.
   A mask fades the last third of the peeking card rather than a gradient
   overlay, so it works on any ground and needs no extra element. */
.footer-quote-grid {
  list-style: none; padding: 0; margin: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: calc((100% - 2 * var(--space-l)) / 3.35);
  gap: var(--space-l);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 88%, transparent 100%);
          mask-image: linear-gradient(to right, #000 0, #000 88%, transparent 100%);
}
.footer-quote-grid::-webkit-scrollbar { display: none; }
/* At the end of the track there is nothing left to peek at, so the fade comes
   off and the last card is fully legible. shell.js sets this. */
.footer-quote-grid[data-fq-end] {
  -webkit-mask-image: none;
          mask-image: none;
}
@media (max-width: 60rem) {
  .footer-quote-grid { grid-auto-columns: calc((100% - var(--space-l)) / 1.6); }
}

/* ==========================================================================
   ONE HERO OFFSET, SITE-WIDE. Chip, 2026-07-29, the last thing before launch.
   The distance from the header to the first thing in a hero was SIX different
   numbers depending on which page you were on: 27 on the homepage, 54 on the
   congress hero, 64 on a sector page, 72 on the deepest pages, and 144 on
   every page whose h1 sits straight under the header with no eyebrow above
   it.
   The deepest pages are the reference (Chip's call), so everything is 72 now.
   --hero-offset is the single value. Change it here and every hero moves
   together; that is the whole point of it existing.
   ========================================================================== */

:root { --hero-offset: var(--space-2xl); }

/* Heroes that carry a .hero editorial: congress, consulting, the sector
   pages. section:has(.hero) zeroes the section, so .hero owns it. */
.hero { padding-block: var(--hero-offset) var(--space-xl); }

/* The homepage band, which puts its padding on the editorial rather than the
   section because the band also has to hold the proof bar. */
.hero-band > .editorial.hero { padding-block: var(--hero-offset) var(--space-m); }

/* Module pages. Already at this value; stated through the token so it cannot
   drift away from the others again. */
.mod-hero { padding-block: var(--hero-offset) var(--space-2xl); }

/* Pages with NO eyebrow, whose h1 sits directly under the header. These were
   inheriting .section--open's --space-4xl, which is the correct interval
   BETWEEN sections and far too much as the first one on a page.
   Scoped to the first section only, so mid-page rhythm is untouched. */
main > section:first-child:not(:has(.hero)) { padding-block-start: var(--hero-offset); }

/* The sector heroes centre their copy against a full-height media column, and
   that centring put the eyebrow 64px lower than every other page on the site.
   Top-aligned so the eyebrow lands on --hero-offset like the rest, and the
   copy column's own padding comes off because the section now supplies it.
   The media column still bleeds full height; only the copy moved. */
.hero-vert-copy { align-self: start; padding-block: 0; }
/* The sector eyebrow is wrapped in a bare <header>, which inherits the body
   line unit (1.55) while the label inside it sets --lh-label. The difference
   is half-leading above the text, and it was putting the sector eyebrows 10px
   lower than every other page. The wrapper takes the label's own leading. */
/* ANY hero eyebrow wrapper, not just the sector one. A <header> or .hero-head
   holding a single inline label still contributes the line box's half-leading
   above it, and that inherited body leading (27.9px against the label's 13.75)
   is what left the sector and app heroes 4 to 10px lower than every other
   page. As a flex container the wrapper's box IS the label's box.
   :has() is already used elsewhere in this file, so no new support floor. */
.hero-vert-copy > header,
.hero-head,
header:has(> .label:only-child) {
  display: flex;
  line-height: var(--lh-label);
}
/* .hero-head carries align-self: end so the kicker sits on the h1's shoulder.
   Where its grid row is exactly the label's height that is identical to start,
   which is every hero but one, where the row is taller and end pushed the
   eyebrow 11px down. start is the no-op everywhere else and the fix there, so
   the offset is one number on every page. */
.hero-head { align-self: start; }
/* .hero distributes its slack with align-content: center against a 85vh
   min-height. Where the content nearly fills that height the centring is
   invisible; where it does not (a 60-word hero) it pushes the eyebrow down,
   which is the last thing making the offset vary by page.
   Top-aligned: the eyebrow now starts at --hero-offset on every page, and the
   slack falls below the hero where it reads as air rather than as a different
   starting position. */
.hero { align-content: start; }

/* ================= 27. THE SPEAKER GRID ================= */
/* Same idiom as .module and .quote: a hairline above, no card, no radius,
 * no shadow, no colour strip. The photograph is the only filled shape on
 * the component, which is what makes a wall of them read as a grid of
 * people rather than a grid of boxes.
 *
 * The portraits are DELIBERATELY inconsistent: some colour, some black
 * and white, several different backdrop eras, and source resolutions
 * from 340 to 600. That is Chip's call, 2026-07-30, made with the
 * tradeoff stated. Do NOT add a greyscale or duotone filter to
 * "unify" them, and do not add a background fill behind the shortfall.
 *
 * Every file is pre-cropped square at the asset level, so aspect-ratio
 * plus cover is belt and braces against one bad file, not the crop
 * mechanism. No width or height attributes are set in the markup because
 * the crops changed twice; aspect-ratio reserves the box, so this still
 * does not shift layout while the images load. */

/* Explicit counts, not auto-fit. Measured: `minmax(9rem, 1fr)` resolved to
 * ONE 311px column at 375, because two 144px tracks plus the gap need
 * 312px and the column is 311. Ten full-bleed portraits stacked single
 * file is a punishing scroll on the device most of this page's readers
 * are holding. Dropping the floor to 8rem bought two columns on a phone
 * but cost a 7-up row on desktop. Fixed counts divide the set cleanly at
 * every width and cannot be lost to a one-pixel rounding change. */
.speaker-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--space-l) var(--space-m);
}
@media (max-width: 60rem) {
  .speaker-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 40rem) {
  .speaker-grid { grid-template-columns: repeat(2, 1fr); }
}

.speaker-card {
  min-width: 0;                  /* a long unbroken name cannot widen the row */
  border-top: var(--rule-hair) solid var(--rule-ink);
  padding-block-start: var(--space-s);
}

.speaker-card-photo {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  object-position: 50% 30%;      /* faces sit above centre in a square crop */
  margin-block-end: var(--space-s);
  background: var(--block);      /* holds the box before the file lands */
}

/* Both are <p>, so the global p rule caps them at --measure. Inside a
 * ~9rem track that never binds, but it does bind at the widest column on
 * a large viewport, which would let a role line run wider than its own
 * photograph. Cap to the card. */
.speaker-card-name,
.speaker-card-role { max-width: 100%; }

.speaker-card-name {
  color: var(--fg-hi);
  font-size: var(--step-0);
  font-weight: 500;
  text-wrap: pretty;
}

.speaker-card-role {
  margin-block-start: var(--space-3xs);
  color: var(--fg-lo);
  font-size: var(--step--1);
  text-wrap: pretty;
}

/* ================= 28. THE PAST-EVENT GALLERY ================= */
/* Documentary photographs of past Congresses. Same hairline idiom as
 * .module, .quote and .speaker-card: a rule above, no card, no radius,
 * no shadow.
 *
 * THE CAPTION IS LOAD-BEARING, NOT DECORATION. The newest photograph
 * ConvergX publishes anywhere is from 2019, and these are 2018 and 2019.
 * A dated photograph presented as the room today is a fabricated
 * impression even when no sentence is false, so every caption names its
 * year. Do not "tidy" the years out of these captions. */

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-l) var(--space-m);
}
@media (max-width: 40rem) {
  .gallery-grid { grid-template-columns: 1fr; }
}

.gallery-figure {
  min-width: 0;
  margin: 0;
  border-top: var(--rule-hair) solid var(--rule-ink);
  padding-block-start: var(--space-s);
}

/* 4:3 is the source shape: every usable frame ConvergX publishes is
 * 864x576 or narrower. Reserving it stops the caption jumping when a
 * lazy image lands, and cover absorbs the one frame that is not 3:2. */
.gallery-img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--block);
}

/* The global figcaption is FIGURE metadata: mono, with its own hairline
 * above. Both are wrong inside a card that already opens on a rule, and
 * a second rule here would double it. Same reset .quote figcaption makes,
 * and for the same reason. */
.gallery-caption {
  margin-block-start: var(--space-s);
  padding-block-start: 0;
  border-top: 0;
  font-family: var(--font-body);
  font-size: var(--step--1);
  color: var(--fg-lo);
  text-wrap: pretty;
}

/* A full-bleed figure's image fills its column. The global img rule sets
 * max-width, which lets a small source sit at natural size and leave dead
 * ground beside it; a bleed figure is the one place that reads as broken
 * rather than as restraint. This replaces an inline width on the Congress
 * header, whose source is 864px against a 1280px bleed. */
.bleed > img { width: 100%; }

/* ================= 29. THE HOMEPAGE LAUNCHERS ================= */
/* The homepage doors: Conference and Consulting. Filled blue boxes on
 * Chip's instruction, 2026-07-30.
 *
 * THIS DELIBERATELY BREAKS THE ONE-ACCENT-FILL RULE and it should be
 * breached knowingly, not quietly. Section 8 says at most ONE solid
 * accent fill per page. The homepage carries these plus the hero Request
 * access button and the nav CTA above them. The rule exists so the accent
 * stays a signal rather than decoration, and every extra fill spends some
 * of that. Chip's call, and the tradeoff was stated. If the accent later
 * stops reading as a signal on this page, this row is the first thing to
 * reconsider, not the hero button.
 *
 * Still true and not negotiable: they are SIBLINGS and must read as
 * siblings. Identical fill, identical padding, no per-door hue, no
 * "recommended" one. The whole point is that the reader picks. */

/* TWO ACROSS. Two divides, so the row goes straight from 2 to 1 at the
 * narrow breakpoint and every stage is a full row with no widow. The
 * intermediate step existed only to keep a wider row from thinning into
 * slivers; a half column never does that. */
.launchers {
  list-style: none;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-l);
}
@media (max-width: 44rem) {
  .launchers { grid-template-columns: 1fr; }
}

/* Air under the row before the next band. Chip asked for it explicitly:
 * filled boxes butting into the following section read as a stack rather
 * than as a set of doors. --space-dense above is the section's own top
 * padding, so only the bottom is added here. */
.launchers-band { padding-block-end: var(--space-3xl); }
@media (max-width: 60rem) {
  .launchers-band { padding-block-end: var(--space-2xl); }
}

.launcher {
  min-width: 0;
  max-width: none;              /* the global li rule caps at --measure */
  /* Column flex so the CTA can be pushed to the bottom. The three
   * descriptions run to different line counts (2 and 3), which left the
   * three CTAs on three different baselines. On a row of siblings that
   * reads as sloppy rather than as variety. */
  display: flex;
  flex-direction: column;
  background: var(--chrome);
  color: var(--chrome-ink);
  /* EVEN on all four sides, Chip's call 2026-07-30 looking at the built
   * thing. This went 35/35/53 asymmetric first, on the theory that a
   * filled box reads bottom-tight because descenders sit near the edge.
   * That theory is sound in general and wrong here: the CTA is underlined,
   * and the underline already adds visual weight below the last baseline,
   * so the optical compensation was being paid twice. One value. */
  padding: var(--space-l);
}

/* The mark slot, and it NO LONGER RESERVES HEIGHT WHILE EMPTY.
 * It did until 2026-07-30, on the reasoning that dropping the three logo
 * files in later should cost no reflow. Measured against what a reader
 * actually sees, that was the wrong trade: an empty 40px band plus its
 * 27px margin put 67px of nothing above the first line of every box, and
 * it read as a padding bug rather than as considered space. Reflow when
 * the marks arrive is cheaper than a row that looks broken until then.
 *
 * The placeholder is a <span> and collapses. A real <img class="launcher-logo">
 * renders, because the rules are split by element. Swap the span for an
 * img and nothing else changes. On the blue fill the mark has to be a
 * reversed/white version; a dark lockup will disappear into the box. */
span.launcher-logo { display: none; }

img.launcher-logo {
  display: block;
  height: 2.5rem;
  width: auto;
  max-width: 100%;
  object-fit: contain;
  object-position: left center;
  margin-block-end: var(--space-m);
}

/* Every child inherits --accent-ink from the box. The label rule paints
 * --label-ink, which is a BLUE and would be invisible here, so it is
 * restated. Same reason .block--accent restates it. */
.launcher-name {
  color: var(--chrome-ink);
  margin-block-end: var(--space-2xs);
}

.launcher-title {
  font-size: var(--step-2);
  margin: 0;
}
.launcher-title a {
  color: var(--chrome-ink);
  text-decoration: none;
}
.launcher-title a:hover { text-decoration: underline; }

.launcher-desc {
  margin-block-start: var(--space-s);
  max-width: none;
  color: var(--chrome-ink);
  font-size: var(--step--1);
  text-wrap: pretty;
}

/* A named destination with a text glyph, never a verb phrase, per the
 * locked tertiary-link rule. Underlined by default rather than on hover:
 * on a filled panel there is no second safe ink, so colour cannot be what
 * marks it as a link. Same reasoning as the notification bar. */
.launcher-cta {
  /* auto, not a fixed step: this is what lands all three CTAs on one
   * baseline whatever the description above it does. The margin below is
   * the box's own bottom padding. */
  margin-block-start: auto;
  padding-block-start: var(--space-m);
  max-width: none;
}
.launcher-cta a {
  color: var(--chrome-ink);
  font-weight: 700;
  font-size: var(--step--1);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.launcher-cta a:hover { text-decoration-thickness: 2px; }

/* ================= 30. THE PHOTOGRAPHIC HERO ================= */
/* Opt-in per page via [data-hero="photo"] on <body>. Only /congress/ uses
 * it. Everything here is scoped to that attribute so the other 63 pages
 * cannot be touched by it.
 *
 * The photograph is the hero's ground: transparent header on top, all
 * hero copy on top, duotone in the accent blue, fading to the page ground
 * at the bottom. No second image file and no build step, so the source
 * asset stays the untouched original and swapping it is one src. */

.hero-photo-band {
  position: relative;
  isolation: isolate;          /* contains the blend modes below */
}

/* Pulled UP behind the nav bar by exactly one bar height so the image
 * starts at the top of the page rather than under the header.
 * --shell-total is measured by shell.js and INCLUDES the notice bar;
 * --shell-h is the bar alone and is the no-JS fallback. The notice is a
 * solid band and paints over the image anyway, so the bar height is the
 * honest number to pull by. */
[data-hero="photo"] .hero-photo {
  position: absolute;
  inset-inline: 0;
  /* DEFAULT IS ZERO, and that is the no-JS case, not an oversight.
   * .shell-header is a class shell.js ADDS; with scripts off it never
   * exists, the <noscript> nav renders in normal flow instead, and there
   * is no sticky bar to hide behind. Pulling up regardless would drag the
   * photograph over the bottom of that nav list. */
  top: 0;
  bottom: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}
/* Only once the real shell has rendered does the photo climb behind it. */
[data-hero="photo"]:has(.shell-header) .hero-photo {
  top: calc(-1 * var(--shell-h));
}

[data-hero="photo"] .hero-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
  /* Flattened to luminance first so the tint below has clean tone to sit
   * on, then pushed back up slightly. Chip asked for duotone but SLIGHTLY
   * saturated, so this is deliberately not a full grayscale(1): a little
   * of the room's own warmth survives under the blue and stops it going
   * flat and screen-printed. */
  filter: grayscale(0.82) contrast(1.06) brightness(0.5) saturate(1.15);
}

/* The tint. `color` blend keeps the image's luminance and takes the hue
 * and saturation from the accent, which is what makes it read as a
 * duotone rather than as a blue sheet laid over a photo. */
[data-hero="photo"] .hero-photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--chrome);
  mix-blend-mode: color;
  opacity: 0.9;
}

/* THE SCRIM, and it is not decoration. White copy over a photograph is
 * only legible by luck unless something guarantees a floor underneath it,
 * and "it looked fine on my screen" is how that ships broken. This lays a
 * ground-coloured wash that is heaviest on the left, where the whole text
 * column sits, and clears off to the right so the room is still visible.
 * Sampled after the fact: see the measured floor in the build notes. */
[data-hero="photo"] .hero-photo-scrim,
[data-hero="photo"] .hero-photo > span {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(to right,
    rgba(10, 10, 10, 0.88) 0%,
    rgba(10, 10, 10, 0.72) 38%,
    rgba(10, 10, 10, 0.30) 72%,
    rgba(10, 10, 10, 0.10) 100%);
}
@media (max-width: 60rem) {
  /* On a phone the copy runs the full width, so a left-weighted wash has
   * nowhere to clear to. Flat and heavier instead. */
  [data-hero="photo"] .hero-photo > span {
    background: rgba(10, 10, 10, 0.78);
  }
}

/* The fade to the page ground, and it sits ABOVE the tint on purpose:
 * it has to resolve to the real --bg or the band edge shows as a seam.
 * Starting at 45% keeps the top of the frame clean where the headline is. */
[data-hero="photo"] .hero-photo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  background: linear-gradient(to bottom,
    rgba(0,0,0,0) 45%,
    var(--bg) 100%);
}

/* THE HEADER GOES TRANSPARENT, but only at rest. The base .shell-header
 * comment is explicit that its background is not optional, because a
 * transparent sticky bar lets content scroll visibly underneath it. That
 * is still true. This only removes the fill while the bar is sitting on
 * the photograph; the moment shell.js marks it floating it takes the
 * ground back and behaves exactly like every other page. */
/* SHARED WITH [data-hero="veil"], AND THIS IS THE ONE THING THE TWO HEROES
 * DELIBERATELY DO SHARE. The heroes themselves are separate components (see
 * the top of section 45 for why), but the header is CHROME: it is not part of
 * either hero, it renders on all 64 pages, and "the bar has no fill while it
 * is sitting on a photograph, and takes one back the moment it is used" is a
 * single behaviour with a single reason. Forking it into two identical copies
 * is how the keyboard case or the open-panel case gets fixed in one and not
 * the other. Adding the second attribute to these four selectors is the whole
 * change; every word of reasoning below still applies verbatim to both. */
[data-hero="photo"] .shell-header,
[data-hero="veil"] .shell-header {
  background: transparent;
  /* The base rule only transitions border-color. Without background here
   * the bar would snap between transparent and grey on hover, which reads
   * as a flicker rather than as a surface arriving. */
  transition: border-color var(--t) linear, background-color var(--t) linear;
}
[data-hero="photo"] .shell-header--floating,
[data-hero="veil"] .shell-header--floating { background: var(--bg); }

/* THE INK HAS TO FOLLOW THE FILL, AND ON THE SECTOR PAGES ONLY. This is the
 * one place [data-hero="veil"] genuinely cannot share the Congress behaviour,
 * and it is a real defect that was measured rather than a tidiness pass.
 *
 * /congress/ carries data-surface="dark" on the <body>, so its nav ink is
 * already the light ink and a transparent bar over a dark photograph reads
 * fine. The eight sector pages are data-surface="light" pages with ONE dark
 * band at the top. Taking the fill off that bar left near-black nav labels
 * sitting on a near-black photograph: measured at 1.02:1 against the composite
 * on /industries/energy/, which is not "low contrast", it is invisible. The
 * logo and the bar's hairline had the same problem.
 *
 * ONLY THE FOUR TOKENS THE BAR ACTUALLY PAINTS WITH ARE MOVED. The nav items
 * take --fg, their hover and current states --fg-hi, the wordmark --logo-ink,
 * the bottom hairline --rule-ink. --bg is deliberately NOT touched: the
 * grounded states below paint background: var(--bg) and that has to stay the
 * page's own paper, or a bar that takes its ground back would come back dark
 * on a light page.
 *
 * THE REVERT IS THE ABSENCE OF THE RULE, which is why this is one selector and
 * not two. Every grounded state is excluded with :not(), so the moment the bar
 * has a fill the rule stops matching, the four tokens fall back to the
 * light-surface values inherited from <body>, and the bar is an ordinary
 * light-page bar again. Restating the light values in a second rule would work
 * today and drift the day [data-surface="light"] is edited.
 *   --floating       scroll state, the bar has taken its ground back.
 *   :hover           pointer.
 *   :focus-within    the same case for a keyboard, which hover never covers.
 *   :has(.mega...)   an open panel.
 * The four match the grounded selectors below exactly. If one is ever added
 * there, add it here in the same edit or the ink and the fill disagree. */
[data-hero="veil"] .shell-header:not(.shell-header--floating):not(:hover):not(:focus-within):not(:has(.mega:not([hidden]))) {
  --fg:        var(--ink);
  --fg-hi:     var(--ink-hi);
  --logo-ink:  var(--ink-hi);
  --rule-ink:  var(--cool-700);
}

/* THE BAR TAKES ITS GROUND BACK WHENEVER IT IS BEING USED. Chip, 2026-07-31.
 * Transparent is right while the bar is just sitting on the photograph; the
 * moment someone reaches for it, nav labels over a busy image are hard work,
 * and an open mega panel hanging off an invisible bar looks detached from it.
 *
 * Three triggers, and each is doing a different job:
 *   :hover        the pointer case Chip asked for.
 *   :focus-within the SAME case for a keyboard, which hover never covers.
 *                 Without it, tabbing into the nav leaves every focused
 *                 link sitting on the photograph.
 *   :has(.mega:not([hidden]))  an open panel. The panels live inside the
 *                 header and toggle the hidden attribute, so this needs no
 *                 JS and cannot drift out of sync with the real state.
 * Deliberately NOT --floating: that one is scroll state and already handled. */
[data-hero="photo"] .shell-header:hover,
[data-hero="photo"] .shell-header:focus-within,
[data-hero="photo"] .shell-header:has(.mega:not([hidden])),
[data-hero="veil"] .shell-header:hover,
[data-hero="veil"] .shell-header:focus-within,
[data-hero="veil"] .shell-header:has(.mega:not([hidden])) {
  background: var(--bg);
}

/* The hero needs real height now that it is carrying an image, and room
 * under the copy so the fade has somewhere to happen. */
[data-hero="photo"] .hero {
  min-height: min(44rem, calc(100svh - var(--shell-total, var(--shell-h))));
  padding-block-end: var(--space-4xl);
}

/* DELETED 2026-07-31: .hero-aside--bare, .hero-provenance, and the
 * [data-hero="photo"] overrides for .hero-category and .hero-provenance.
 * The Congress hero's <aside> is gone from the markup (Chip: wider copy
 * block, less vertical height), and those four rules were written for it
 * and nothing else, so they now match nothing on any of the 64 pages.
 * The BASE .hero-aside rules in section 17 are untouched and still live.
 * Section 31 below carries the replacement geometry.
 *
 * THE ONE PLACE THE BLUE EYEBROW RULE CANNOT APPLY, and it STAYS. Every
 * other eyebrow on the site is --label-ink, which is a blue. This one sits
 * on a BLUE PHOTOGRAPH, where blue-on-blue measured as unreadable and
 * looked it. Lifted to the emphasis ink: --ink-hi on the scrimmed
 * photograph, and the scrim's own floor is what carries it. Scoped as
 * tightly as possible, the photo hero only. */
[data-hero="photo"] .hero-head .label { color: var(--ink-hi); }

/* ================= 31. THE CONGRESS HERO, ASIDE REMOVED ================= */
/* Chip, 2026-07-31: the hero copy block gets wider and shorter. The aside
 * that held "Tenth year." and the 2018 provenance line is deleted from the
 * markup, which frees columns 9 to 12 on this page and this page only.
 *
 * SCOPED TO [data-hero="photo"], WHICH IS /congress/ AND NOTHING ELSE.
 * Checked before writing rather than assumed, because the brief that
 * ordered this said the homepage still carries an aside and it does not.
 * The homepage hero has head, title, sub and cta only, and has never had an
 * aside. The [data-hero="photo"] scope keeps this off every other hero.
 *
 * BOTH THE COLUMN AND THE MEASURE MOVE, BECAUSE THEY WERE TIED. Measured
 * live at 1280 rather than assumed: the content box is 1110px, so a column
 * is 68.1px and the column gap 26.6px. .display--hero carries
 * max-width: var(--measure-hero), 12ch, which resolves to 737px there,
 * against a span-8 column of 731px. The two were within 6px of each other,
 * so BOTH were binding and moving either one alone would have changed
 * nothing on screen. Column to 10 of 12 (920px), measure to 16ch (983px).
 * The column is the governor by design.
 *
 * SPAN 9 WAS TRIED FIRST AND MEASURED AS A FAILURE. At 826px the live
 * headline still rendered FOUR lines, exactly as it did at 737px, so the
 * whole point of the change (Chip: less vertical height) was not bought.
 * 920px is where it drops to three, which is about 90px of height back.
 * This is the reason to measure a line count rather than reason about a
 * character count: 826 and 920 look like a small difference on paper and
 * are a whole line apart on screen.
 *
 * AND THE SCRIM MOVES WITH IT, see the override at the end of this
 * section. At span 10 the copy box ends at 1005px, about 79% across the
 * viewport, which on the ORIGINAL gradient would have been roughly 0.25
 * alpha and genuinely too thin for white type. Re-weighting the wash to
 * follow the copy is the right half of that trade; starving the headline
 * to fit a gradient authored for a narrower column is the wrong half.
 * main-end is still refused: past 93% there is no honest amount of wash
 * that leaves the photograph visible at all. */
[data-hero="photo"] .hero-head,
[data-hero="photo"] .hero-title,
[data-hero="photo"] .hero-cta   { grid-column: col-start 1 / span 10; }
[data-hero="photo"] .hero-title { max-width: 16ch; }

/* The lede is NOT widened by measure. --measure-lede is 34ch, the authored
 * reading measure for running copy, and stretching it to fill new space is
 * the one change here that would cost legibility rather than buy it. Only
 * the column moves, from 5 to 6, and only because below about 1100px the
 * 5-column track resolves NARROWER than 34ch and starts binding instead of
 * the measure. At 1280 this is a no-op by design. */
[data-hero="photo"] .hero-sub { grid-column: col-start 1 / span 6; }

/* Height comes down with the copy. 44rem to 36rem, and the bottom pad from
 * 5 body lines to 4. The floor is the fade: the gradient in section 30
 * starts at 45% of the photo box and has to resolve to --bg by 100%, so
 * there has to be real height under the last line for it to land in.
 * --space-3xl is 96 to 108px, which is more than the fade needs and is a
 * line-commensurate step, so the rhythm rules in 6b still hold. */
/* THE FOOTPRINT IS NOW A CONTRACT, NOT A NUMBER, and that is the only reason
 * it moved out of the rule below. Section 45 builds a second photographic hero
 * for the eight sector pages and its brief is that it occupy the SAME vertical
 * footprint as this one. Two components carrying the same handwritten
 * `min(36rem, calc(...))` would agree until the day somebody tuned one of them,
 * and then they would disagree silently, on eight pages nobody was looking at.
 * One custom property, read by both, is the difference between "they match"
 * and "they cannot drift apart".
 *
 * WHAT THE EXPRESSION MEANS, unchanged from when it was written inline:
 * 36rem is the authored height. The calc caps it at whatever is actually left
 * of the viewport under the chrome, so a short landscape phone never gets a
 * hero taller than its own screen. --shell-total is measured by shell.js and
 * includes the notice bar; --shell-h is the bar alone and is the no-JS
 * fallback, which is why the fallback is inside the var() and not a separate
 * rule. Changing this value changes BOTH heroes. That is the point. */
:root { --hero-photo-h: min(36rem, calc(100svh - var(--shell-total, var(--shell-h)))); }

[data-hero="photo"] .hero {
  min-height: var(--hero-photo-h);
  /* Was --space-3xl, 107px, measured as dead space below the sponsor strip.
   * --space-2xl still clears what the fade needs: the gradient starts at 45%
   * of the photo box and must resolve to --bg by 100%, and at this hero
   * height that is roughly 480px of run for a 64px pad to sit inside. If the
   * hero ever gets much shorter, re-check the fade before cutting further. */
  padding-block-end: var(--space-2xl);
}

/* THE MOBILE COLLAPSE HAS TO BE RESTATED, and the first version of this
 * section did not restate it and was broken. Section 17's collapse rule is
 * `.hero-head, .hero-title, .hero-sub, .hero-cta { grid-column: main }`,
 * specificity 0,1,0. The rules above are 0,2,0 and BEAT it, so the span-9
 * placement kept applying inside a grid that no longer has nine content
 * columns, `col-start 1 / span 9` resolved into the right-hand rail, and
 * the document scrolled sideways by a full gutter at both 375 and 768.
 * Caught by measuring the scrollWidth, not by reading the rule. Anything
 * scoped to an attribute has to re-win every media query it lands in. */
@media (max-width: 60rem) {
  [data-hero="photo"] .hero-head,
  [data-hero="photo"] .hero-title,
  [data-hero="photo"] .hero-sub,
  [data-hero="photo"] .hero-cta   { grid-column: main; }
  [data-hero="photo"] .hero-title { max-width: var(--measure-hero); }
}

/* THE SCRIM FOLLOWS THE COPY. Section 30 authored this gradient against a
 * span-8 column whose text ended near 64% of the viewport, so it spent its
 * weight in the first 38% and cleared to 0.10 by 72%. The copy now runs to
 * 71%, which on the original stops is about 0.31 alpha, and white type over
 * a photograph at that floor is legible by luck rather than by design.
 *
 * The heavy stretch is extended and the clear-off pushed right. Measured
 * against where the copy box actually ends, 1005px of 1280, which is 78.5%:
 * this gradient interpolates to about 0.50 alpha there, against about 0.25
 * on the original stops. It is also HIGHER than the 0.40 the old, narrower
 * copy enjoyed at its own 64%, so the widening costs nothing in legibility
 * and buys a little. The right 15% of the frame still clears to 0.14, so
 * the room is still visible and the photograph is still doing its job.
 *
 * Same near-black wash and the same reasoning as the base rule, so it is
 * stated the same way. This is the ONE component that cannot resolve its
 * wash through a surface token: it has to composite against image pixels,
 * not against a ground, which is why section 30 wrote a literal here and
 * why this override does too. Both are scoped to the photo hero alone.
 *
 * GATED TO min-width, WHICH IS NOT TIDINESS. Section 30's mobile rule
 * flattens this wash to a single heavy value below 60rem, and it carries
 * the SAME specificity as this selector, 0,2,0. A media query adds none.
 * So an unconditional override here would sit later in the file and beat
 * the flat wash at every width, taking a left-weighted gradient onto the
 * one layout where the copy runs full width and the wash has nowhere to
 * clear to. 60.0625rem is 961px, exactly one pixel past the max-width
 * breakpoint, so the two never both apply. */
@media (min-width: 60.0625rem) {
  [data-hero="photo"] .hero-photo-scrim,
  [data-hero="photo"] .hero-photo > span {
    background: linear-gradient(to right,
      rgba(10, 10, 10, 0.88) 0%,
      rgba(10, 10, 10, 0.78) 60%,
      rgba(10, 10, 10, 0.40) 85%,
      rgba(10, 10, 10, 0.14) 100%);
  }
}

/* ================= 32. THE NOTIFICATION BAR, REBUILT ================= */
/* Chip, 2026-07-31: bigger, centred, a live countdown, and a real Register
 * button. Section 7d still owns the bar's ground and ink and is unchanged.
 * This section is the layout, the type step and the CTA on top of it.
 *
 * THE BAR RENDERS ON 52 PAGES. Nothing here is scoped to /congress/ on
 * purpose: the bar is chrome and has to look the same everywhere. What
 * that costs is that every rule below has to be surface-independent, and
 * it is: the bar states its own ground (--accent) and its own ink
 * (--accent-ink), so it resolves identically on dark, light and muted.
 *
 * CONTRAST, ALL RE-MEASURED 2026-07-31 AGAINST THE CURRENT BLUE.
 * Ignore any figure written above the stale-ratio warning line at the top
 * of this file; those were computed against the retired orange.
 *   --accent-ink (white) on --accent (#146ef5)   4.59:1   body and countdown
 *   --paper fill on --accent                     4.59:1   the button's edge
 *   --accent-text on --paper                     6.46:1   the button's label
 *   --cool-100 fill on --accent                  4.02:1   hover edge
 *   --accent-text on --cool-100                  5.67:1   hover label
 * The two fill figures are the WCAG 1.4.11 non-text boundary and need
 * 3:1. Both label figures are normal text and need 4.5:1. All five clear.
 *
 * MOTION. Nothing in this component animates. The colour transition on the
 * CTA comes from the single global rule in section 19, and the
 * prefers-reduced-motion block directly under it zeroes every transition
 * and animation on the page with !important, so it governs this bar
 * already and a second query here would be dead weight. */

.notice-inner {
  /* Sized down again on 2026-07-31 after Chip saw it: --step-0 read a touch
   * large for chrome, and --space-s block padding made the band deeper than
   * a notification needs. --step--1 with --space-xs keeps it a band rather
   * than fine print, without competing with the page. */
  padding-block: var(--space-xs);
  font-size: var(--step--1);

  display: flex;
  /* ONE LINE, AT EVERY WIDTH. Chip, 2026-07-31, looking at the built bar.
   * nowrap is the requirement, and it is the reason every rule below the
   * tier ladder exists: once the row cannot wrap, something has to give
   * when it runs out of width, and WHAT gives has to be authored rather
   * than left to whatever the flex algorithm reaches for first. */
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* ONE SENTENCE, NOT TWO ITEMS. Chip: the countdown should align with the
   * date line and the two should feel continuous. They are separate flex
   * children, so the gap between them IS the space between the sentences,
   * and at --space-l it read as two things sitting near each other. A word
   * space is what makes prose continuous, so that is what this now is.
   * The tier ladder tightens it further at narrow widths and still works,
   * because this is the same property it was already stepping down. */
  gap: var(--space-2xs);
}

/* The button is the one thing that must NOT read as part of the sentence,
 * so it takes back the separation the row just gave up. Margin rather than
 * a second gap value: gap is one value for the whole row and the tiers are
 * already spending it. */
.notice-cta { margin-inline-start: var(--space-m); }

/* BOTH FLEX CHILDREN ARE <p>, WHICH DRAGS IN TWO GLOBAL RULES THAT ARE
 * WRONG HERE. The global p rule caps every paragraph at --measure, which
 * would park the sentence off-centre in a centred row the moment the bar
 * is wider than 50ch. And `p + p` in section 2 puts --space-s of
 * margin-block-start on the second one: as a flex item in a ROW that is a
 * top margin, so the countdown would sit one paragraph-gap lower than the
 * sentence beside it. A margin on a flex item is not collapsed and not
 * absorbed by gap, exactly as the note at line 430 records for grid. Both
 * killed for both children, once. */
.notice-text,
.notice-countdown { max-width: none; margin: 0; }

/* min-width: 0 is what lets a flex item shrink below its content width.
 * Without it the date string sets a floor the row cannot get under, and a
 * nowrap row that cannot shrink pushes the page sideways instead, which is
 * the trade this must not make. The CTA is deliberately NOT in here: it
 * carries flex: none further down, because a Register button squeezed to
 * "Regis..." is worse than anything the ladder gives up. */
.notice-text,
.notice-countdown { min-width: 0; }
.notice-text { white-space: nowrap; overflow: hidden; }

/* THREE STATES, NOT ONE, and the layout is the same rule for all three.
 * The bar renders as text plus countdown plus button, as text plus button
 * once the script removes the countdown after the target passes, and as
 * text plus button with scripts off. Nothing here counts children or
 * targets a position, so a two-child row centres exactly like a three-child
 * one. The same is true INSIDE the countdown: under reduced motion the
 * script drops the seconds unit, so the row renders with three units on
 * some machines and four on others. justify-content plus one gap value is
 * correct at both counts. There is no nth-child and no first/last-of-type
 * anywhere in this section, and no rule assumes four units.
 *
 * ================== THE ORDER OF SACRIFICE ==================
 * One line is the requirement, so the row has to give something up as the
 * viewport narrows. This is the authored order, cheapest first, and each
 * tier is a real breakpoint below.
 *
 *   1. Gaps tighten.                    Costs nothing but air.
 *   2. .cd-l unit labels clip away.     "53 : 08 : 41 : 02" still reads as
 *                                       a clock. CLIPPED, not display:none,
 *                                       so "days" and "hrs" stay in the
 *                                       accessibility tree. Colon separators
 *                                       come in with this tier, as generated
 *                                       content, so they are punctuation and
 *                                       not markup.
 *   3. .cd-label clips away.            Same technique, same reason: the
 *                                       prefix that names what is being
 *                                       counted down is still announced.
 *   4. Type steps down one rung.        --step-0 to --step--1 on the date.
 *
 * NOT SPENT, AND ON PURPOSE:
 *   - The seconds unit is never hidden. Measured at 375 it is not needed:
 *     tiers 1 to 4 bring the row to about 300px inside a 311px box. Hiding
 *     it would also have meant a rule keyed to data-cd="seconds", and that
 *     attribute is a script hook.
 *   - The date is never dropped. It was the last resort in the ladder and
 *     the measurement never reached it.
 * Every hidden thing on this list is clipped, so NOTHING in this bar is
 * removed from the accessibility tree at any width. */

/* ---- TIER 1: gaps tighten, from 64rem down ---- */
@media (max-width: 64rem) {
  .notice-inner { gap: var(--space-m); }
  .notice.is-live .notice-countdown { gap: var(--space-2xs); }
}

/* ---- TIER 2: the unit labels clip, colons arrive, from 48rem down ---- */
/* The clip block is .vh from section 24, restated rather than reached for,
 * because .vh is a class on an element and these elements carry .cd-l. */
@media (max-width: 48rem) {
  .cd-l {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%);
    overflow: hidden;
    white-space: nowrap;
  }
  /* The separator goes on .cd-n, INSIDE the unit's own line box, so it
   * cannot become a second flex item in the column and cannot push
   * anything onto a new line. `+` means it is never on the first unit, at
   * three units or at four, so the count is never assumed. */
  .cd-unit + .cd-unit .cd-n::before {
    content: ":";
    margin-inline-end: 0.15em;
    /* The colon is punctuation between two numbers that are already
     * announced with their own labels, so it adds nothing for a screen
     * reader and costs nothing either. */
  }
  .notice-inner { gap: var(--space-s); }
}

/* ---- TIER 3 and 4: the prefix clips, the type steps down, from 30rem ---- */
/* 30rem is 480px, so 375, 390 and 414 are all inside this tier and all
 * three were measured. */
@media (max-width: 30rem) {
  .cd-label {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%);
    overflow: hidden;
    white-space: nowrap;
  }
  .notice-inner { gap: var(--space-2xs); font-size: var(--step--1); }
  .notice.is-live .notice-countdown { gap: var(--space-3xs); }
  .cd-n { font-size: var(--step--1); }
}

/* ---- the countdown ---- */
/* HIDDEN BY DEFAULT, AND THAT IS THE NO-JS STATE, NOT AN OVERSIGHT. Same
 * idiom and same selector shape as section 26 (.quotes.is-live
 * .quote-track): everything outside .is-live is the component without
 * JavaScript, and the enhanced rules only exist once the script has
 * confirmed it can drive the thing. The gate lives on .notice, not on the
 * countdown itself. Never move a base rule into .is-live.
 *
 * display: none, NOT opacity OR visibility. The .cd-n elements ship
 * carrying 00 and only mean anything after the first tick, so a merely
 * transparent countdown is still a row of zeros holding a row of layout,
 * and revealing it would reflow the whole bar sideways. Taking it out of
 * flow entirely is the only version where the bar before the first tick
 * and the bar with scripts off are the SAME bar. */
.notice .notice-countdown { display: none; }

.notice.is-live .notice-countdown {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: var(--space-2xs) var(--space-s);
  color: var(--chrome-ink);
}

/* The prefix, e.g. "Congress opens in". Mono and label-cased so it reads
 * as the clock's caption rather than as a second sentence. NOT .label:
 * that class paints --label-ink, which is a blue, and would be blue on
 * blue here. Same restatement .block--accent and .launcher-name make. */
.cd-label {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  line-height: var(--lh-label);
  color: var(--chrome-ink);       /* 4.59:1 at 11px, clears the 4.5:1 floor */
}

.cd-unit {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: var(--lh-label);
}

.cd-n {
  font-family: var(--font-mono);
  /* Tabular, so the digits keep one advance and the row does not shuffle
   * sideways on every tick. The mono stack is already fixed-advance, and
   * this is stated anyway because the fallback face at the end of that
   * stack is only mono by name on some systems. */
  font-variant-numeric: tabular-nums;
  font-size: var(--step-1);
  font-weight: 700;
  color: var(--chrome-ink);       /* 4.59:1 */
}

.cd-l {
  font-family: var(--font-mono);
  font-size: var(--step--2);      /* 11px, so it needs the full 4.5:1 */
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  color: var(--chrome-ink);       /* 4.59:1, clears it */
}

/* ---- the Register button ---- */
/* THE PROBLEM: .btn--solid paints --accent on --accent. A solid accent
 * button on the accent bar is a 1:1 rectangle, which is to say it is
 * invisible, and the contract puts .btn--solid on this element.
 *
 * THE FIX IS TO INVERT IT, not to outline it. An outlined white button
 * would have been legal (white border on blue is 4.59:1, over the 3:1
 * boundary floor) and it would have read as the site's DEFAULT button
 * state, which is exactly the wrong signal for the one control the bar
 * exists to point at. A filled white box with blue type is unambiguous,
 * uses only existing tokens, and inverts the same accent pairing rather
 * than introducing a second one.
 *
 * SPECIFICITY. Written as .notice-inner a.notice-cta, which is 0,2,1.
 * That beats both .btn--solid (0,1,0) and the existing .notice-inner a
 * rule in section 7d (0,2,0) on weight rather than on source order, so
 * this cannot be silently lost if the file is ever reordered. That is the
 * failure the note at line 2074 records. */
.notice-inner a.notice-cta {
  background: var(--paper);       /* on --accent: 4.59:1, over the 3:1 boundary floor */
  border-color: var(--paper);
  color: var(--accent-text);      /* on --paper: 6.46:1 */
  text-decoration: none;
  /* flex: none, so the ladder above never spends the one control the bar
   * exists to point at. A Register button squeezed to an ellipsis is worse
   * than every tier the ladder gives up. */
  flex: none;
  white-space: nowrap;
  /* TIGHTER THAN .btn, and only here. Chip, 2026-07-31. The base padding
   * is var(--space-xs) var(--space-l), sized for a standalone button
   * sitting in a page. In a chrome bar that reads as a slab. .btn itself is
   * untouched: it renders on all 64 pages and this is one control on one
   * component.
   * VERTICAL PADDING IS NOT CUT AS FAR AS THE HORIZONTAL, deliberately. It
   * is the only thing left setting the hit area. Measured live, the control
   * comes out 39px tall at 375 and 41px at 1280, which is short of the 44px
   * touch guideline and is a real, named cost of the one-line requirement.
   * Cutting the vertical to buy width would have taken it into the twenties
   * and made a chrome control genuinely hard to hit. The hero carries a
   * full-size Register button to the same destination, so this bar is not
   * the only way to reach it. */
  padding: var(--space-2xs) var(--space-s);
  font-size: var(--step--1);
}
@media (max-width: 30rem) {
  .notice-inner a.notice-cta { padding: var(--space-2xs) var(--space-xs); }
}
.notice-inner a.notice-cta:hover {
  background: var(--cool-100);    /* on --accent: 4.02:1, still over 3:1 */
  border-color: var(--cool-100);
  color: var(--accent-text);      /* on --cool-100: 5.67:1 */
}
/* :active in section 8 repaints --fg-hi, which is a light grey on the dark
 * ground and would drop the button to near nothing for the length of the
 * press. Held to the same inverted pair. */
.notice-inner a.notice-cta:active {
  background: var(--cool-100);
  border-color: var(--cool-100);
  color: var(--accent-text);
}

/* ================= 33. THE STICKY SECTION SUBNAV ================= */
/* .subnav > .subnav-list > li > a. Sticks BELOW the main header.
 *
 * THE OFFSET, CHECKED AGAINST THE REAL RULE. .shell-header is
 * position: sticky; top: 0 (line 645) and carries a border-bottom that is
 * ALWAYS in the box and only changes colour, so the header's rendered
 * height is the nav bar's min-height plus one hairline. --shell-h is
 * declared on html in section 1 and its own comment says "constant. The
 * bar does not resize on scroll", so the sum is exact rather than
 * approximate: calc(var(--shell-h) + var(--rule-hair)).
 *
 * WITH JAVASCRIPT OFF THE OFFSET IS ZERO, and that is the correct value,
 * not a fallback. .shell-header is a class shell.js ADDS. With scripts off
 * it never exists, the <noscript> nav renders in normal flow, and there is
 * no sticky bar to sit under. Offsetting anyway would leave a bar-height
 * strip of page visible above the subnav with nothing in it. Same
 * :has(.shell-header) idiom the photographic hero already uses at line
 * 2978, for the same reason.
 *
 * The notice bar is deliberately NOT in this sum. It sits before the
 * header in the flow and scrolls away (see buildNotice in shell.js), so
 * adding its height would push the subnav down by a band that is not there
 * any more by the time anything is stuck. */

:root {
  /* A floor, not a guess: .subnav-list enforces it as min-height below, so
   * the anchor arithmetic further down can rely on it. Touch targets clear
   * 44px inside it with room to spare. */
  --subnav-h: 3.5rem;
}

.subnav {
  position: sticky;
  top: 0;
  z-index: 10;                    /* under .shell-header (20) and .skip (40), over the page */

  /* WHITE AT ALL TIMES, not only when stuck. Chip, 2026-07-31.
   * The bar carries data-surface="light", so --bg here IS the light ground
   * and this one declaration makes it white in flow and white floating.
   *
   * It was transparent in flow, which meant it took whatever sat behind it.
   * The dark hero and the impact statement are directly above, so the bar
   * read as dark until the instant it stuck and then flipped to white. The
   * FLIP was the defect, not the colour.
   *
   * A container-width top rule used to live here too. It existed to separate
   * a transparent bar from the sponsor marks above it; a bar with its own
   * ground separates itself, so the rule became a line drawn across the top
   * of a white block for no reason and went with the transparency.
   *
   * THREE EARLIER COMMENTS WERE DELETED WITH IT, not edited: they described
   * a no-band-until-stuck behaviour, a top-rule-swaps-for-bottom-rule
   * behaviour, and a full-bleed-versus-container-width argument, none of
   * which this component does any more. Stale comments on this project have
   * already cost real time. */
  background: var(--bg);

  /* Hairline, matching every other rule on the site. Always in the box so
   * the bar never changes height and the page never shifts by the 1px a
   * real border would add. Colour arrives with .subnav--stuck.
   * The ACTIVE MARKER stays --rule-med on purpose: it is the one thing in
   * the bar that has to be louder than the bar, and a 2px accent segment on
   * a 1px neutral rule reads as a marker rather than as more of the line. */
  border-bottom: var(--rule-hair) solid transparent;
  transition: border-color var(--t) linear;
}


/* The stuck state. Ground is not optional once it floats: a transparent
 * sticky band lets content scroll visibly through it.
 * The rule is a UI component boundary under WCAG 1.4.11 and needs 3:1.
 * --rule-ink-load is --cool-500 on dark (3.89:1 on --ground) and --cool-400
 * on light and muted (3.50:1 on --paper). Both clear. */
/* Stuck now changes ONE thing: the bottom rule arrives. The ground is
 * already white in flow, so there is nothing to swap. The rule is what
 * separates the bar from content moving underneath it, which is only a
 * thing that happens once it floats. */
.subnav--stuck {
  border-bottom-color: var(--rule-ink-load);
}
@media (prefers-reduced-motion: reduce) {
  .subnav { transition: none; }
}
body:has(.shell-header) .subnav { top: calc(var(--shell-h) + var(--rule-hair)); }

/* ---- HIDDEN UNTIL IT STICKS ------------------------------------------
 * Chip, 2026-08-04: "the sub menus on the different pages should be hidden
 * until you hit the first section. Otherwise, they feel like they're just
 * floating randomly."
 * He is right about the cause. Sitting in flow under a hero, the bar is a
 * naked row of small links with no ground of its own and nothing above or
 * below to attach it to, so it reads as debris. It only becomes legible as
 * navigation at the moment it pins to the header, because that is when it
 * gains a job.
 *
 * .subnav--stuck ALREADY MARKS EXACTLY THAT MOMENT, so this needs no new
 * state and no new script. shell.js toggles that class from a sentinel at
 * the bar's own offsetTop, and re-asserts it inside mark() for the
 * backgrounded-tab case. Both paths use an explicit boolean, so they cannot
 * fight, and this rule inherits that for free.
 *
 * VISIBILITY, NOT OPACITY ALONE, and the difference matters. opacity: 0
 * leaves every link focusable and hit-testable, so a keyboard user tabbing
 * through the hero would land inside an invisible menu. visibility: hidden
 * takes it out of the tab order and out of hit testing, and it is still
 * animatable when its transition is delayed to the end of the fade.
 *
 * IT KEEPS ITS SPACE IN FLOW, deliberately, and this is the one tradeoff.
 * A sticky element that collapses to zero height cannot reliably reach its
 * own offset, and removing the height would also snap the first section up
 * the page the instant the bar appeared. So the bar's height stays as an
 * empty band under the hero. On every page carrying a subnav that band
 * paints the page's own ground, so it reads as breathing room rather than
 * as a gap. If a future surface makes it read as a gap, the fix is a
 * negative margin on the FOLLOWING section, never display: none here.
 *
 * NO-JS IS THE REASON FOR THE :has() GUARD. .shell-header is a class
 * shell.js adds, so with scripts off it never exists, this rule never
 * matches, and the bar renders permanently visible as a plain row of
 * working anchors. Hiding it unconditionally would leave a no-JS reader with
 * no in-page navigation at all. A browser without :has() support degrades
 * the same safe way.
 *
 * NOT THE SAME THING as the deleted "no band until stuck" behaviour the
 * comment at the top of this section records. That one changed the bar's
 * BACKGROUND on stick and was removed because the flip was the defect. This
 * changes whether the bar is present at all, and the background stays
 * constant throughout. Do not merge the two. */
body:has(.shell-header) .subnav {
  visibility: hidden;
  opacity: 0;
  transition: opacity 240ms ease-out, visibility 0s linear 240ms;
}
body:has(.shell-header) .subnav.subnav--stuck {
  visibility: visible;
  opacity: 1;
  transition: opacity 240ms ease-out, visibility 0s linear 0s;
}
@media (prefers-reduced-motion: reduce) {
  body:has(.shell-header) .subnav,
  body:has(.shell-header) .subnav.subnav--stuck { transition: none; }
}

/* The band paints the full width; the list takes the page container, the
 * same way .nav-bar sits inside .shell-header. That is what keeps the
 * subnav's first link on the site's single left edge. */
.subnav-list {
  position: relative;
  max-width: var(--page-max);
  margin-inline: auto;
  padding: 0 var(--gutter);
  min-height: var(--subnav-h);
  list-style: none;

  display: flex;
  align-items: center;
  gap: var(--space-l);

  /* SCROLLS RATHER THAN WRAPPING. Six or seven section names wrap to three
   * lines at 375 and turn a 56px band into a 150px one that is stuck to the
   * top of the screen for the whole page. A single scrolling row keeps the
   * band one row high at every width.
   *
   * NO PAGE OVERFLOW: the scroller is max-width --page-max with its own
   * padding, so it is never wider than its container and the overflow is
   * consumed inside it. 100% is used nowhere here and 100vw nowhere at all,
   * per the note at line 341.
   *
   * overscroll-behavior-x: contain is what stops a swipe that reaches the
   * end of the row from turning into a back navigation. Same rule and same
   * reason as .quotes.is-live .quote-track. The scrollbar is NOT hidden
   * here: the carousel has arrow buttons as its affordance and this has
   * none, so the bar itself is the only signal there is more to the right. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
}
.subnav-list > li { flex: none; max-width: none; }   /* the global li rule caps at --measure */
.subnav-list > li > a {
  display: block;
  scroll-snap-align: start;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--step--1);
  color: var(--fg);               /* 12.58:1 dark, 17.93:1 light, 15.73:1 muted */
  text-decoration: none;
  white-space: nowrap;
  padding-block: var(--space-xs);
}
.subnav-list > li > a:hover { color: var(--fg-hi); }   /* 15.73:1 dark, 17.93:1 light */

/* THE CURRENT-SECTION AFFORDANCE, AND WHAT IT CANNOT DO WITHOUT JS.
 * A real scroll-spy needs to know which section is in view, and CSS has no
 * way to reach backwards from a section to a link that precedes it in the
 * DOM. :target does not help either: it only fires on a fragment click and
 * goes stale the moment the reader scrolls past. Scroll-driven animations
 * could fake it and would be a moving indicator that reduced-motion has to
 * reason about, for an affordance nobody asked for.
 * So this styles the state and does not attempt to compute it: set
 * aria-current="true" on the link and the rule below inks it. If nothing
 * ever sets the attribute, the subnav still works and simply has no
 * current marker. The inline device is the site's existing current-branch
 * treatment, an underline drawn as a border, no fill and no pill. */
/* The marker sits ON the bar's own bottom edge rather than floating above
 * it. Chip, 2026-07-31. Drawn as an absolutely positioned pseudo, not a
 * border, so the current item occupies exactly the same box as an inactive
 * one and nothing shifts as the marker moves between links. Same technique
 * and same reasoning as the main nav's current-branch device.
 * The marker is --rule-med against the bar's --rule-hair, deliberately: it
 * sits ON that edge and has to be legible as a separate mark on it. */
.subnav-list > li > a[aria-current] {
  color: var(--fg-hi);
  position: relative;
}
.subnav-list > li > a[aria-current]::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* The list has block padding; this reaches past it to the container edge
   * so the segment lands on the border, not above the padding. */
  bottom: 0;
  border-bottom: var(--rule-med) solid var(--accent);   /* 3.91:1 dark, 4.59:1 paper, over the 3:1 boundary floor */
}

/* bottom: 0 only lands on the bar's edge if the link box actually REACHES
 * that edge. It did not: the list centres its items, so each link was a
 * content-height box floating in the middle of the bar and the marker was
 * measured 8px past the border, guessing at a padding value that was never
 * there. Stretching the items and centring the text INSIDE them makes the
 * link box the full height of the bar, so the marker's own bottom edge and
 * the bar's bottom border are the same line by construction rather than by
 * arithmetic. Nothing about the text position changes. */
.subnav-list { align-items: stretch; }
/* stretch, NOT center, on the li as well. Centring it made the <a> a 47px
 * content box floating inside a 56px row, so the marker drawn at the link's
 * own bottom still sat 7px short of the bar's border. The chain has to
 * stretch the whole way down: list to li to a. The text stays centred by
 * the flex centring INSIDE the link, which is the last step. */
.subnav-list > li { display: flex; align-items: stretch; }
.subnav-list > li > a { display: flex; align-items: center; }

/* THE ANCHOR OFFSET, and this is the detail that is usually missed. An
 * in-page jump scrolls the target to y=0, which on this page is underneath
 * two stacked sticky bars, so the heading lands hidden and the reader sees
 * the second paragraph. scroll-margin-top pushes the resting position down
 * by the full chrome plus one body line of air.
 *
 * Scoped with :has so only a page carrying a subnav pays it. [id] rather
 * than section[id] because the fragment can legitimately target a heading
 * or a wrapper, and a scroll-margin on an element nobody ever jumps to
 * costs nothing. The JS-off branch drops the header term for the same
 * reason section 33's top offset does. */
body:has(.subnav)                      { --anchor-offset: calc(var(--subnav-h) + var(--space-m)); }
body:has(.subnav):has(.shell-header)   { --anchor-offset: calc(var(--shell-h) + var(--rule-hair) + var(--subnav-h) + var(--space-m)); }
body:has(.subnav) [id]                 { scroll-margin-top: var(--anchor-offset); }

/* SMOOTH SCROLLING, Chip 2026-07-31. The resting position was already right,
 * courtesy of the scroll-margin above; what was missing was the travel, so a
 * subnav click teleported and the reader lost their place on the page.
 *
 * ON html, NOT body. scroll-behavior only does anything on the element that
 * actually scrolls, which is the document element; set on body it silently
 * does nothing in every browser that matters.
 *
 * SCOPED WITH :has, the same way the offset above is scoped, so only a page
 * carrying a subnav pays for it. That is /congress/ today and it is the page
 * Chip asked for. It deliberately does NOT apply site wide: the skip link and
 * the FAQ disclosures also move the viewport, and animating those is a
 * different decision that nobody has asked for.
 *
 * REDUCED MOTION IS ALREADY HANDLED, at section 17, which forces
 * scroll-behavior: auto !important on everything. Do not add a second
 * override here; one is enough and two drift apart. */
html:has(.subnav) { scroll-behavior: smooth; }

/* ================= 34. THE HERO SPONSOR STRIP ================= */
/* .hero-sponsors wraps the EXISTING .proofbar and .mark components from
 * section 25. Nothing about a mark is rebuilt here: the same masked
 * artwork, the same --mark-cap sizing, the same centred row. This is the
 * container and its ground, and that is all it is.
 *
 * THE GROUND IS THE WHOLE PROBLEM. On /congress/ the strip sits inside a
 * photographic hero. .mark paints itself with --fg-hi through a mask, and
 * --fg-hi over a duotone photograph has no floor that can be computed: the
 * scrim in section 30 runs from 0.88 alpha on the left to 0.10 on the
 * right, and what is under it is image pixels, so the right-hand marks
 * would be light artwork on whatever the room happened to be. There is no
 * honest ratio to write for that, which is the reason not to ship it.
 *
 * So under the photo hero the strip states its own ground, and it states
 * it as --bg rather than a fixed colour, so the pairing is --fg-hi on --bg
 * and is correct on all three surfaces by construction:
 *   dark   --ink-hi on --ground    15.73:1
 *   light  --ink-900 on --paper    17.93:1
 *   muted  --ink-900 on --cool-100 15.73:1
 * This is NOT a card. No radius, no shadow, no border except the house
 * hairline above it, and the fill is the page's own ground, which is what
 * the bottom of the hero has already faded to by the point the strip sits
 * at. FOR THE MARKUP AGENT: put the strip LAST inside the hero band. Place
 * it higher and the --bg band will read as a bar cutting across the
 * photograph, because the fade in section 30 has not resolved yet. */

/* NO RULE AND NO GROUND. Chip, 2026-07-31: the marks sit directly on
 * whatever is behind them. The house hairline-above idiom was the wrong
 * borrow here, because every other component that uses it (.module,
 * .quote, .speaker-card, .gallery-figure) is a row in a LIST of siblings,
 * where the rule is what separates one from the next. A single strip of
 * marks has nothing to be separated from, so the rule was drawing a line
 * under the hero copy rather than dividing anything. */
.hero-sponsors {
  padding-block-start: var(--space-m);
  /* Was --space-xl. Measured: the CTA's own block-end margin and this one
   * compounded to a 107px gap, which was the largest hole in the hero and
   * the main reason it read too tall. */
  margin-block-start: var(--space-m);
}
/* A leading label sits above the row, not beside it. */
.hero-sponsors > .label { display: block; margin-block-end: var(--space-s); }

/* Scoped: only the photographic hero needs the guaranteed ground. On an
 * ordinary page the strip is already on the page's own ground and a second
 * fill would be a rectangle drawn over a rectangle. */
/* On the photo hero it keeps its stacking context, so the marks paint over
 * the image rather than under it, but it takes NO ground. Chip's call: the
 * strip reads as part of the photograph, not as a panel laid on top of it.
 * The tradeoff is real and is accepted: the marks now sit on whatever the
 * duotone happens to be behind them rather than on a guaranteed --bg, so
 * there is no fixed contrast figure for them. The scrim in section 30 is
 * what keeps that region dark, and the strip sits in the lower left where
 * the scrim is heaviest. If a future hero photograph is brighter there,
 * this is the thing that breaks first. */
[data-hero="photo"] .hero-sponsors {
  position: relative;
  z-index: 1;                     /* over .hero-photo, which sits at -1 */
  padding-block-start: var(--space-m);
}

/* ================= 35. THE IMPACT STATEMENT ================= */
/* Full-width band under the hero, set large.
 *
 * TYPE STEP: --step-4, WITH --track-4. Not --step-6: that step is the hero
 * h1 and is spent exactly once per page, and /congress/ has already spent
 * it on the .display--hero above this band. Not --step-5 either, because
 * that is the default h1 rank and this statement is not the page's title.
 * --step-4 is the ladder's own mid-page voice, defined in section 2 as
 * "below the h1, above the h2", which is precisely the job. It is the same
 * step .display and .pull take, so this reads as a sibling of the devices
 * already on the site rather than as a new size.
 *
 * COLOUR: BOTH OF CHIP'S OPTIONS, ONE RULE PAIR, PICKED BY THE MARKUP.
 * .impact-text is the emphasis ink, .impact-em is the link blue. Wrap ONE
 * phrase in .impact-em and you get white with the emphasis in blue. Wrap
 * the WHOLE sentence in .impact-em and you get the all-blue version. No
 * modifier class, no second rule to keep in sync, and the markup decides
 * by how much it wraps.
 *
 * MEASURED 2026-07-31:
 *   .impact-text  --fg-hi on --bg      15.73:1 dark, 17.93:1 light, 15.73:1 muted
 *   .impact-em    --link on --bg        5.30:1 dark (--accent-lift),
 *                                       6.46:1 light and 5.67:1 muted (--accent-text)
 * --link, never --accent: the flat accent is 3.91:1 on the dark ground,
 * which is a fill figure and fails AA as type. That is the same trap the
 * .label comment in section 3 records. */

/* Air on BOTH sides, Chip 2026-07-31. The section previously took the house
 * top-padded-only rule, which is right for a section in a run of sections and
 * wrong for this one: it is a single statement standing between a photographic
 * hero and the light ground below it, and it needs room to be a beat rather
 * than a heading. --space-4xl over and under, symmetrical, because there is no
 * next-section padding underneath it to borrow from now that the band changes
 * ground immediately after.
 *
 * NOT symmetrical in the stylesheet, because symmetrical is not what reads as
 * centred here. Measured: the sponsor strip above contributes its own 71px of
 * margin before this section even starts, so equal padding put roughly 213px
 * over the text and 142px under it. The extra --space-xl on the bottom is that
 * 71px paid back. Optical centring against what is actually above, which is
 * the logo row, not the section boundary. */
.impact { padding-block: var(--space-4xl) calc(var(--space-4xl) + var(--space-xl)); }
@media (max-width: 60rem) {
  .impact { padding-block: var(--space-3xl) calc(var(--space-3xl) + var(--space-m)); }
}

/* FULL WIDTH, CENTRED, AND NO MORE THAN THREE LINES. Chip, 2026-07-31.
 * --step-3, not --step-4: at step-4 across the full container this ran to
 * five lines, and the brief is three. The measure cap is removed because a
 * 22ch display measure is what was forcing the wrap in the first place;
 * the container is the measure here.
 * text-wrap: balance stays and is doing real work at three lines: it evens
 * the ragged edge instead of leaving a two word last line. */
.impact-text {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 600;
  letter-spacing: var(--track-3);
  line-height: var(--lh-heading);
  /* SWAPPED, Chip 2026-07-31: the setup sentence is now the accent and the
   * payoff is the ink. --link, never --accent: the flat accent is 3.91:1 on
   * the dark ground and fails as type, where --link resolves to the lifted
   * blue at 5.30:1 dark, 6.46:1 paper, 5.67:1 muted. */
  color: var(--link);
  max-width: none;
  margin-inline: auto;
  text-align: center;
  text-wrap: balance;
}

/* Swapped with .impact-text on 2026-07-31: this is now the INK, not the
 * accent. 15.73:1 on dark. */
.impact-em {
  color: var(--fg-hi);
  /* No weight change, no underline, no background. The colour IS the
   * emphasis, and a second signal on top of it turns a statement into a
   * callout. Declared explicitly because .impact-em will usually be a
   * <strong>, and the global strong rule repaints --fg-hi and would cancel
   * the blue. */
  font-weight: inherit;
}

/* ================= 36. WHAT IS THE CONGRESS ================= */
/* .whatis > .whatis-row > (.whatis-title + .whatis-body). Title left,
 * description right, stacked on small screens.
 *
 * THE MEASURE AND THE BASELINE COME FROM THE EDITORIAL IDIOM, not from a
 * second grid invented here. Section 6 puts .lede on col-start 1 / span 5
 * and .body on col-start 8 / main-end, and aligns the two on their FIRST
 * BASELINE rather than on their boxes, because a larger left column and a
 * body-size right column starting at the same box top start their type a
 * few pixels apart and that is exactly the misalignment the eye catches
 * and the ruler does not. The same 5 / gap / 8-to-end proportion and the
 * same align-self are reproduced here.
 *
 * WHY NOT LITERALLY .editorial. That grid carries two rail tracks that
 * resolve to the page edge, which is its job as a PAGE grid. A row inside
 * an already-wrapped section does not want a second set of rails; it wants
 * the twelve content columns only. Twelve columns and the same column gap
 * is the honest subset, and it lines up with the page grid because both
 * are twelve equal tracks over the same content box. */

.whatis-row {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));   /* minmax(0,...): a long unbroken string cannot blow the row out */
  column-gap: var(--space-m);                          /* --col-gap, the editorial value */
  align-items: baseline;                               /* the editorial baseline rule, not box alignment */

  /* The house component idiom again: a hairline above, no card, no radius,
   * no shadow. Same as .module, .quote, .speaker-card, .gallery-figure. */
  border-top: var(--rule-hair) solid var(--rule-ink);
  /* Asymmetric: more air UNDER each block than over it. Chip, 2026-07-31.
   * The rule above each row already separates it at the top, so equal
   * padding read as the text crowding the next row's rule. */
  padding-block: var(--space-m) var(--space-xl);
}

/* A third and two thirds, Chip 2026-07-31. 4 and 8 of a 12 column grid is
 * exactly that. The previous 5 / 8-to-end was neither: it left a dead two
 * column gutter in the middle and made the title column wider than a third. */
.whatis-title { grid-column: 1 / span 4; }
.whatis-body  { grid-column: 5 / -1; }

/* Stated rather than inherited from an element, because the contract does
 * not say what element .whatis-title is and an h2, an h3 and a <p> would
 * otherwise render this row at three different sizes on three pages. Same
 * treatment .link-index a takes: display face, step-2, weight 500. The
 * class beats any bare element selector on specificity, so an h3 here
 * lands on the same size as a paragraph. */
.whatis-title {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: var(--track-2);
  line-height: var(--lh-heading);
  color: var(--fg-hi);
  max-width: var(--measure-lede);
  text-wrap: balance;
}

/* --measure, the body prose value, about 66 characters. If .whatis-body is
 * a <p> the global rule already sets it; restated so a <div> wrapper gets
 * the same column. */
/* The COLUMN is two thirds and the text now fills it. --measure (50ch, about
 * 66 characters) was capping the paragraph at roughly half the row, so the
 * grid said two thirds and the eye saw half.
 * THE TRADEOFF IS REAL AND IS CHIP'S CALL, 2026-07-31: at this row width the
 * line runs to roughly 80 characters, which is past the 45 to 75 band the
 * measure token exists to hold. It is acceptable HERE because these are four
 * short blocks read one at a time, not a long article. Do not copy this
 * override onto running body copy elsewhere. */
.whatis-body { max-width: none; }
.whatis-body > * { max-width: none; }

/* One column below 60rem, matching where .editorial itself collapses. The
 * column gap has nothing to sit between once there is one track, so the
 * interval becomes a row gap instead of disappearing. */
@media (max-width: 60rem) {
  .whatis-row {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-s);
  }
  .whatis-title, .whatis-body { grid-column: 1 / -1; }
}

/* ================= 37. THE OUTCOME BLOCK ================= */
/* Six short items.
 *
 * IT ECHOES THE .link-index HAIRLINE ROW, AND IT DOES NOT TAKE ITS TYPE
 * STEP. The hairline-above row is the site's list idiom and it is used by
 * five components already, so a sixth list that opened with something else
 * would read as an import. What .link-index does on TOP of that is set its
 * rows at --step-2, and the comment there says why: on the router pages
 * the destinations ARE the payload. These are not destinations and not the
 * payload; they are six supporting statements under a heading. At step-2
 * they would outrank the heading above them and read as a second menu.
 * So: the rule, the rhythm and the ledger feel of .link-index, at body
 * size, in the emphasis ink.
 *
 * Two columns from 60rem up. Six short items in one column is a tall thin
 * ribbon with a lot of dead ground beside it; six in two columns of three
 * is a block, which is what the component is called. */

/* ONE COLUMN, real bullets, hairlines between. Chip, 2026-07-31.
 * Two columns made six short items read as a matrix with an implied
 * relationship across the gutter, which there is not: it is one list.
 * The marker is drawn rather than list-style, because list-style::marker
 * cannot be reliably coloured separately from its text in every engine and
 * this one has to be the accent while the text stays --fg. */
.outcome-list {
  list-style: none;
  padding: 0;
  display: block;
  max-width: var(--measure);
  /* Breathing room after the intro line ("Attendees leave with:"), Chip
   * 2026-08-13: the first row drops its own top padding, so without this
   * the list sat flush against the sentence introducing it. */
  margin-block-start: var(--space-m);
}
.outcome-list > li {
  position: relative;
  padding-inline-start: var(--space-m);
  padding-block: var(--space-xs);
  border-top: var(--rule-hair) solid var(--rule-ink);
  max-width: none;
}
.outcome-list > li:first-child { border-top: 0; padding-block-start: 0; }
.outcome-list > li::before {
  content: "";
  position: absolute;
  left: 0;
  /* Centred on the first LINE, not on the item: a two line entry with a
   * vertically centred dot reads as a dot floating between two lines. */
  top: calc(var(--space-xs) + (1em * var(--lh-body) / 2));
  width: 0.4em;
  height: 0.4em;
  margin-block-start: -0.2em;
  border-radius: 50%;
  background: var(--chrome);
}
.outcome-list > li:first-child::before { top: calc(1em * var(--lh-body) / 2); }
@media (max-width: 60rem) {
  .outcome-list { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
}

.outcome-list > li {
  min-width: 0;
  max-width: none;                /* the global li rule caps at --measure and these size to the column */
  border-top: var(--rule-hair) solid var(--rule-ink);
  padding-block: var(--space-s);
  color: var(--fg-hi);
  text-wrap: pretty;
}

/* ---- the hero's two lede columns, 2026-07-31 ----------------------------
 * Chip: side by side, one size, matched line lengths, because the stacked
 * pair at two different sizes made the hero too tall.
 *
 * The wrapper takes the grid cell and subdivides it, rather than the two
 * paragraphs each claiming their own columns off the hero grid. Two reasons.
 * Placing them individually would have meant :nth-of-type on .hero-sub, which
 * counts <p> siblings and would silently re-target the moment anyone adds a
 * paragraph to the hero. And equal tracks inside one wrapper are what makes
 * the LINE LENGTHS match, which is the thing Chip actually asked for: same
 * measure, same wrap point. Two hero-grid spans would only match while the
 * outer grid stayed symmetrical.
 *
 * max-width: none on the children is load-bearing. The global p rule caps at
 * --measure (50ch) and .hero-sub inherits a lede measure; either would bind
 * before the track does and the two columns would stop being equal. */
[data-hero="photo"] .hero-subs {
  grid-column: col-start 1 / span 10;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--space-l);
  row-gap: var(--space-m);
}
[data-hero="photo"] .hero-subs > .hero-sub {
  grid-column: auto;
  max-width: none;
  /* Zeroed because the stacking margin still applies inside a grid cell.
   * Measured: the second paragraph sat 18px lower than the first, which is
   * the rhythm rule's block-start margin doing exactly its job in a context
   * that no longer wants it. Two columns of a pair are siblings across, not
   * down, so the row-gap is what separates them when they wrap on a phone
   * and nothing should separate them here. */
  margin-block-start: 0;
}

/* Below the collapse the two tracks become one. Two 5-column measures at
 * 768 are about 22ch each, which is a newspaper column, not a lede. The
 * base rule in section 17 already sends .hero-sub to `main`; the wrapper
 * needs the same treatment or it keeps its 10-column span inside a
 * one-column grid, which is exactly the overflow bug measured earlier. */
@media (max-width: 60rem) {
  [data-hero="photo"] .hero-subs {
    grid-column: main;
    grid-template-columns: 1fr;
  }
}

/* ================= 38. THE COUNTDOWN AS PROSE ================= */
/* SUPERSEDES the widget treatment in section 31. Chip, 2026-07-31: the
 * units must all be the same size on one line, "Opening remarks in" is
 * gone, and the whole thing reads as a sentence.
 *
 * That is not a restyle of the widget, it is a different component, so
 * the widget's assumptions are undone here rather than edited in place:
 * its own comments and its sacrifice ladder stay readable as the record of
 * what was tried before Chip saw it.
 *
 * WHAT CHANGES, and each of these was a real defect once the markup became
 * prose rather than a clock:
 *
 * 1. NO FLEX, NO GAP. The separators are now real text (", " and " and ")
 *    living in .cd-sep. With flex gap they rendered as `52 DAYS , 20 HOURS`
 *    with the comma detached from its word, because gap inserts space
 *    between flex items and the comma is its own item. textContent was
 *    always right; only the painting was wrong. Inline flow puts the
 *    spacing back where prose spacing belongs, in the text itself.
 * 2. ONE TYPE for .cd-n, .cd-l, .cd-sep and .cd-label. The widget set the
 *    number at --step-1 bold mono and the labels at 11px uppercase mono
 *    with label tracking, which is exactly the two-treatment look Chip
 *    rejected. Body face, body size, normal case, no tracking.
 * 3. THE COLON SEPARATORS ARE CANCELLED. Tier 2 injected ":" via
 *    .cd-unit + .cd-unit .cd-n::before for the narrow end. Real commas now
 *    exist at every width, so the generated colons would double them.
 * 4. .cd-l STOPS CLIPPING at the narrow end. "days" is a word in a
 *    sentence now, not a widget caption, and clipping it would leave
 *    "Only 52, 20, 15 and 37 away." which is not English.
 *
 * Tabular figures are KEPT on .cd-n. The seconds digit changes every tick
 * and a proportional face would shuffle the whole sentence sideways. */

.notice.is-live .notice-countdown {
  display: block;
  gap: 0;
  max-width: none;
  text-align: center;
  color: var(--chrome-ink);
}

.notice .cd-label,
.notice .cd-unit,
.notice .cd-n,
.notice .cd-l,
.notice .cd-sep {
  display: inline;
  font-family: var(--font-body);
  font-size: inherit;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  line-height: inherit;
  color: var(--chrome-ink);       /* white on the accent bar, 4.59:1 */
}
.notice .cd-n { font-variant-numeric: tabular-nums; }

/* Cancels the tier-2 generated colon. The commas are real text now. */
.notice .cd-unit + .cd-unit .cd-n::before { content: none; }

/* Undo the tier-2 and tier-4 clip and step-down: both were widget
 * behaviours. The narrow-end budget is met by the bar's own type step on
 * .notice-inner, which still applies. */
@media (max-width: 48rem) {
  .notice .cd-l {
    position: static;
    width: auto; height: auto;
    margin: 0; clip-path: none;
    overflow: visible;
    white-space: normal;
  }
}
@media (max-width: 30rem) {
  .notice .cd-label {
    position: static;
    width: auto; height: auto;
    margin: 0; clip-path: none;
    overflow: visible;
    white-space: normal;
  }
  .notice .cd-n { font-size: inherit; }
}


/* ---- "What the Congress is": no rule over the heading ------------------
 * Chip, 2026-07-31, second time of asking. The eyebrow came off this
 * .sec-head earlier; the hairline the component draws above itself is the
 * other half and goes now.
 *
 * Scoped to this one section rather than changed on .sec-head, which is
 * used on roughly ninety headings across the site and whose rule is a real
 * structural device everywhere else. The heading also loses the padding
 * that only existed to hold the text off that rule. */
#about > .wrap > .sec-head {
  border-top: 0;
  padding-block-start: 0;
}

/* ---- the agenda's day headings, 2026-07-31 -----------------------------
 * Chip: a BIG title per date, and the day opens to the full timetable.
 * The three summaries used to carry a sentence describing the day; they are
 * now the date alone, so the type has to do the work the sentence did.
 *
 * --step-2, one rung above the .faq default, and NOT larger: these sit under
 * an h2 at --step-3 and a date that outranks the section's own heading reads
 * as the page restarting. The measure cap on .faq summary > span comes off,
 * because a five word date has no wrap to control and the cap was only ever
 * there to hold a paragraph. */
.day-title {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: var(--track-2);
  line-height: var(--lh-heading);
  max-width: none;
}
/* The affordance grows with the title so the plus does not sit small beside
 * it, and the row gets more air now that each one is a single big line. */
#agenda .faq > summary { padding-block: var(--space-m); }
#agenda .faq > summary::after { font-size: var(--step-2); }

/* ---- Congress page: bare section titles, and a full-width agenda --------
 * Chip, 2026-07-31.
 *
 * 1. THE TWO BARE TITLES GO UP A RUNG. #speakers and #agenda lost their
 *    eyebrows and their standfirsts, so the heading is the only thing
 *    introducing each section and --step-3 was carrying a job it used to
 *    share. --step-4, not higher: the h1 is --step-6 and stays the page's
 *    one dominant, and --step-5 is h1 rank.
 *    SCOPED to those two on purpose. #about keeps --step-3 because it still
 *    has four subheadings under it; a section head that outranks its own
 *    children reads as the page restarting.
 *
 * 2. THE AGENDA RUNS THE FULL COLUMN. .mod-faq-list carries a max-width
 *    (measured at 781px inside a 1109px column) because on a module page it
 *    holds prose answers that need a reading measure. Here it holds a
 *    TIMETABLE, where the constraint was squeezing three-column rows into
 *    two thirds of the available width for no legibility gain. */
/* ---- REVISED 2026-07-31, Chip: all four section titles are one thing ----
 * "The 'Questions' title should be the same size as The Speakers and The
 * Agenda... you should also use the same styling for that title... I'd be OK
 * if these titles were even larger and you had more white space on top of
 * each of them... none of them need a top border over the titles."
 *
 * SO #about AND #faq JOIN THE SET. Note 1 above scoped this to two sections
 * and reasoned that #about had to stay at --step-3 because it carries four
 * .whatis-title subheadings and a head that outranks its own children reads
 * as the page restarting. That reasoning does not survive contact with the
 * built page: .whatis-title is --step-2, so --step-4 over it is an ordinary
 * two-step gap, exactly the gap every other h2 on the site has over its own
 * subheads. The rule was solving a problem that was not there.
 *
 * THE RULE IS GONE, not overridden. A section heading on this page is one
 * size, full stop, and there is no longer a list of exceptions to keep in
 * sync. #about's heading also changed text, from "What the Congress is" to
 * "Congress overview", on the same instruction.
 *
 * NO TOP BORDER on any of the four. The hairline was the site's device for
 * separating a section from the one before it, and on a page whose sections
 * are now introduced by a 57px title it is doing that job twice. The space
 * below is the separation.
 *
 * MORE AIR ABOVE. --space-4xl replaces the section default. The border came
 * off, so the interval has to carry the boundary on its own, and Chip asked
 * for it directly. Bottom padding is untouched: sections on this site are
 * top-padded only and that is what keeps one interval between any two. */
#about  .sec-head h2,
#speakers .sec-head h2,
#agenda .sec-head h2,
#accommodations .sec-head h2,
#faq    .sec-head h2 {
  font-size: var(--step-4);
  letter-spacing: var(--track-4);
}
#about  .sec-head,
#speakers .sec-head,
#agenda .sec-head,
#accommodations .sec-head,
#faq    .sec-head {
  border-top: 0;
  padding-block-start: 0;
}
#about, #speakers, #agenda, #accommodations, #faq { padding-block-start: var(--space-4xl); }
/* #agenda and #faq carry .section--dense, which sets a SMALLER top padding
 * and would otherwise win on source order. Restated at the same specificity
 * so the four really do land on one interval. */
#agenda.section--dense,
#accommodations.section--dense,
#faq.section--dense { padding-block-start: var(--space-4xl); }
/* THE ONE SECTION ON THIS PAGE WITH BOTTOM PADDING, and it is an exception to
 * a rule, not a relaxing of it. Sections here are TOP padded only, which is
 * what keeps exactly one interval between any two of them however they are
 * ordered. That works while neighbours share a ground: the interval is one
 * undivided piece of the page and it does not matter which side owns it.
 * #faq now paints its own light ground (data-surface="muted"), so the boundary
 * is a visible seam and the whole interval sits INSIDE the grey. The agenda's
 * last line ended hard against that seam with nothing under it while the grey
 * carried the lot. This splits the interval across the seam.
 * SCOPED TO #agenda DELIBERATELY. It is not a licence to bottom pad sections
 * generally, and doing that would double every interval on the page. The test
 * for another exception is the same one: does this section's neighbour paint a
 * different ground. */
/* MOVED FROM #agenda TO #accommodations, 2026-08-04, and the rule above is the
 * reason rather than an exception to it. The test it states is "does this
 * section's neighbour paint a different ground". When it was written, Agenda was
 * the last light section and FAQ's muted ground followed it. Accommodations was
 * inserted between them on 2026-08-04 and the seam moved; the padding did not.
 * MEASURED before the change, at 1440: agenda to accommodations was 249px, a
 * light-to-light seam carrying the exception, while accommodations to faq, where
 * the ground actually changes, got the standard 142px. So the page had dead air
 * in the middle of a band and no extra interval at its one real boundary. Chip
 * flagged this page as needing padding work and this is what he was seeing.
 * If a section is ever inserted after Accommodations, re-check this the same
 * way: find the surface change, put the exception on the section before it. */
#accommodations { padding-block-end: var(--space-3xl); }
/* SAME EXCEPTION, SECOND SEAM, 2026-08-04. Chip: "there needs to be more
 * padding at the bottom of this here", on the Questions list running into the
 * blue app band.
 * The rule above states the test: does this section's neighbour paint a
 * different ground. #faq is muted and .app-band is the solid accent, so it
 * passes the test just as clearly as #accommodations does, and it was getting
 * only the standard interval. MEASURED at 1440 before the change: 142px, on the
 * page's hardest edge. A full-bleed colour band gives a list nowhere to end,
 * so this seam needs the interval MORE than the light-to-muted one, not less.
 * Scoped to #faq, not to sections generally, for the reason the rule above
 * gives: bottom-padding every section would double every interval on the page. */
#faq { padding-block-end: var(--space-3xl); }
/* Same cap, same reason, both released: .mod-faq-list carries a reading
 * measure for prose answers on a module page. The agenda holds a timetable
 * and the FAQ holds three grouped columns of questions; neither gains
 * anything from being squeezed to two thirds of the column. */
#agenda .mod-faq-list,
#accommodations .mod-faq-list,
#faq .mod-faq-list { max-width: none; }

/* The group headings. Small, quiet, and the thing that makes this read as
 * three decisions rather than one long list. */
.faq-group {
  font-family: var(--font-mono);
  font-size: var(--step--2);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  line-height: var(--lh-label);
  color: var(--fg-lo);
  margin-block: var(--space-xl) var(--space-s);
}
.faq-group:first-of-type { margin-block-start: var(--space-m); }
#agenda .faq-a table  { width: 100%; }

/* ---- the agenda timetable, 2026-07-31 ---------------------------------
 * 1. THE TIME NEVER WRAPS. "07:30 to 08:00" was breaking across two lines
 *    and turning every row into a two line row for no reason.
 * 2. THE TIME COLUMN IS THE SAME WIDTH ON ALL THREE DAYS. It was auto
 *    sized per table, so Tuesday, whose session names are short, gave the
 *    column far more room than it needed and Wednesday, whose names are
 *    long, squeezed it until it wrapped. width: 1% with nowrap is the
 *    standard shrink-to-content idiom: the browser gives the column
 *    exactly its content width and hands the rest to the session column,
 *    and because every cell holds the same "HH:MM to HH:MM" shape the
 *    result is identical on all three days without a magic number.
 * 3. The visible date caption inside each day was a repeat of the heading
 *    the reader just opened. It is .vh now rather than deleted, because a
 *    caption is the table's accessible NAME and three tables called
 *    nothing is a worse outcome than one duplicated line. */
#agenda .faq-a table th[scope="row"] {
  white-space: nowrap;
  width: 1%;
  padding-inline-end: var(--space-l);
}

/* ==========================================================================
   39. THE PERSON BIO OVERLAY. 2026-07-31.
   Used by /congress/#speakers and /about/leadership/. Appended at the END of
   the file for the reason recorded at section 26's tail: anything added from
   here on wins on source order rather than on specificity.

   THE BASE IS :target AND IT IS THE WHOLE COMPONENT.
   Same discipline as the quote carousel at section 26. Everything above the
   `.has-bio-open` rule works with JavaScript fully disabled: a card is a link
   to `#bio-<slug>`, the overlay is a normal element that :target reveals, and
   the close control is a link back to the card's own id, which both clears the
   target and returns the reader to the card they left. That is a working modal
   with zero script. <dialog> was rejected outright: showModal() needs JS and
   the no-JS run would get an inert element and nothing else.
   NEVER move a base rule into a script-gated class.

   WHAT THE SCRIPT ADDS, additively: Escape, focus into the panel on open and
   back to the trigger on close, a focus trap, and the scroll lock below.

   CONTRAST, computed against WCAG 2.1 relative luminance for the overlay's
   own ground, which is --ground #171717 (the panel declares
   data-surface="dark" so all six tokens below resolve there):
     --fg-hi   #f0f0f0  name          15.73:1   clears AAA
     --fg      #d8d8d8  bio prose     12.58:1   clears AAA
     --fg-lo   #ababab  title line     7.81:1   clears AAA
     --rule-ink-load #757575 head rule and close boundary
                                       3.89:1   clears 1.4.11's 3:1 UI floor
     close glyph stroke, --fg-hi      15.73:1   graphical object, floor is 3:1
   NOT USED, and this is the trap the brief names: --accent #146ef5 measures
   3.91:1 on this ground. That is a legal FILL and an illegal piece of type.
   --link resolves to --accent-lift #3b89ff, 5.30:1, and is the only blue that
   may carry type here. Nothing in this component is accent-coloured.

   MOTION: the fade is the only transition, it is one --t (120ms), and the
   global prefers-reduced-motion block at section 19 already zeroes it. No
   second reduced-motion query is written here, because a duplicate would be a
   second place to keep in step.
   ========================================================================== */

/* A column, not a scroll box. The head holding the close control does not
 * scroll and the body does, so the X is still in the top right at the bottom
 * of a 354-word bio. A single overflow:auto box loses it off the top. */
.bio-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;                  /* over .skip at 40, which is the site's ceiling */
  display: flex;
  flex-direction: column;
  visibility: hidden;
  /* THE CLOSED OVERLAY MUST NOT HIT-TEST, and opacity alone does not stop it.
   * The close transition holds visibility: visible for --t before flipping it,
   * so for 120ms after a close this fixed inset:0 element is invisible and
   * still swallows every click landing on it. A reader who closes one bio and
   * immediately clicks the next card loses that click.
   * Worse under prefers-reduced-motion: the global reduce block zeroes
   * transition-DURATION but not transition-DELAY, so the fade is instant and
   * the dead zone is not, leaving no visual cue that anything is in the way. */
  pointer-events: none;
  opacity: 0;
  /* visibility is delayed OUT so the fade is visible, and switched IN with no
   * delay so the panel is never a click target before it is drawn. */
  transition: opacity var(--t) linear, visibility 0s linear var(--t);
}
html:not(.js) .bio-overlay:target,
.bio-overlay.is-open {
  /* .is-open (2026-08-14): the script opens overlays via pushState, which
   * does NOT recompute :target. The class is the script's ONLY reveal
   * channel; :target reveals only without script (html:not(.js)), because
   * a stale :target ALSO survives the script's replaceState close: a page
   * entered at #bio-<x> kept that overlay invisibly "on" and, sitting last
   * in the DOM, it painted over whoever was actually opened (Chip's
   * Kim-shows-Frances report, 2026-08-14). */
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transition: opacity var(--t) linear, visibility 0s linear 0s;
}

.bio-head {
  flex: 0 0 auto;
  display: flex;
  justify-content: flex-end;
  border-bottom: var(--rule-hair) solid var(--rule-ink-load);
}

/* The body is the scroll container, and overscroll-behavior is load-bearing
 * rather than polish: it is what stops a wheel or a swipe at the end of a bio
 * from chaining into the page underneath. With the script off that is the only
 * thing holding the background still, since the class-based lock below never
 * gets applied. */
.bio-scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-block: var(--space-xl) var(--space-3xl);
}

/* A real 4rem target, not a glyph with padding. The accessible name is the
 * .vh "Close" inside it; the SVG is two lines and is aria-hidden, so a screen
 * reader never reads a multiplication sign as a word. */
.bio-close {
  display: grid;
  place-items: center;
  inline-size: var(--space-2xl);
  block-size: var(--space-2xl);
  color: var(--fg-hi);
  text-decoration: none;
  border-inline-start: var(--rule-hair) solid var(--rule-ink-load);
}
.bio-close svg {
  inline-size: var(--space-m);
  block-size: var(--space-m);
  display: block;
}
.bio-close:hover { background: var(--block); }

/* Photograph and text. minmax(0, …) on both tracks so a long unbroken string
 * in a bio cannot widen the row and start a horizontal page scroll. The photo
 * track tops out at 18rem (288px), which sits under the smallest source file
 * in either set (Tracy LaTourette's, 340px square), so nothing is ever
 * upscaled by CSS at any viewport. */
.bio-figure {
  display: grid;
  grid-template-columns: minmax(0, 18rem) minmax(0, 1fr);
  gap: var(--space-l) var(--space-xl);
  align-items: start;
}
@media (max-width: 48rem) {
  .bio-figure { grid-template-columns: minmax(0, 1fr); }
  .bio-photo  { max-inline-size: 18rem; }
}

/* aspect-ratio holds the box before the file lands, so opening an overlay
 * never reflows the panel. Every file in both sets is already square at the
 * asset level; cover is insurance against one future non-square file, not the
 * crop mechanism. */
.bio-photo {
  display: block;
  inline-size: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--block);
}

.bio-name {
  color: var(--fg-hi);
  font-size: var(--step-2);
  letter-spacing: var(--track-2);
}
/* The title-and-employer line. --fg-lo is de-emphasis and never body copy;
 * this is metadata about the person, which is the job it is for. */
.bio-role {
  margin-block-start: var(--space-2xs);
  color: var(--fg-lo);
  font-size: var(--step--1);
  text-wrap: pretty;
}
.bio-text > p + p { margin-block-start: var(--space-s); }
.bio-prose { margin-block-start: var(--space-m); }

/* THE TRIGGER. A real link carrying the person's name as its accessible name,
 * with a stretched pseudo-element so the whole card is the hit area.
 * Deliberately NOT a wrapping <a> around the card: on /about/leadership/ the
 * card is a subgrid item spanning three rows and a wrapper would collapse it
 * to one, and using display:contents to dodge that drops the link out of the
 * accessibility tree in shipping browsers. One idiom serves both pages. */
.speaker-card,
.modules--people > .module { position: relative; }

.bio-trigger {
  color: inherit;
  text-decoration-thickness: var(--rule-hair);
  text-underline-offset: 0.18em;
}
/* SCOPED 2026-08-14 to the two parents that position themselves for it.
 * Unscoped, every trigger on /about/'s team section stretched over the SAME
 * containing block (the cards there are not positioned), and the last one in
 * the DOM swallowed every real click in the region: clicking Kim's View full
 * bio opened Frances. Programmatic .click() bypasses hit-testing, which is
 * why automated checks kept passing. */
.speaker-card .bio-trigger::after,
.modules--people > .module .bio-trigger::after {
  content: "";
  position: absolute;
  inset: 0;
}
.bio-trigger:hover { text-decoration-thickness: var(--rule-med); }
/* The focus ring is drawn on the link's own text box, which is a few words
 * inside a tall card. Without this the ring is legible but does not tell the
 * reader which card is about to open. */
.speaker-card:has(.bio-trigger:focus-visible),
.modules--people > .module:has(.bio-trigger:focus-visible) {
  outline: var(--rule-med) solid var(--focus);
  outline-offset: var(--space-3xs);
}

/* ---- the script-gated state, and it is one rule ---- */
/* shell.js sets this while an overlay is open and records the scroll offset so
 * it can put it back. With the script absent the class is never applied and
 * the overscroll-behavior above is what holds the background. */
.has-bio-open, .has-bio-open body { overflow: hidden; }

/* ================= 40. THE STOREFRONT ================= */
/* 2026-07-31, CHIP'S CALL: "build these pages as if they are the actual
 * stores." /congress/register/ and /congress/sponsor/ ONLY. Nothing else on
 * the site sells anything, and nothing else may borrow these classes.
 *
 * WHAT CHANGED, AND WHY IT IS NOT A REVERSAL. Both pages previously carried
 * their products as table rows with a single CTA at the foot, under a rule
 * that read "tiers are rows, never columns, no per-row action." That rule was
 * written to stop a comparison matrix with a Buy button on every line, which
 * is a pricing-page pattern that manufactures urgency. What is built here is
 * the other thing: one product, one price, one list of what is in it, one way
 * to buy it. The parts of that rule that were about pressure rather than
 * about layout ALL STILL HOLD and are enforced below in the negative:
 *   no badge, no ribbon, no "most popular", no highlighted or scaled row,
 *   no strike-through, no countdown, no seat or stock count, no "from",
 *   no savings maths, no per-item urgency of any kind.
 * If a future pass wants any of those, that is a Chip decision, not a CSS one.
 *
 * NOT CARDS IN THE DECORATIVE SENSE, which is why --radius stays 0 and no
 * shadow appears anywhere here. On the muted ground --block resolves to
 * --paper, so an item is a white field on light grey: the elevation is a
 * lightness step, the same device section 9 already uses. Consistent with
 * the house rule that a container earns its keep by separating data, not by
 * decorating it. */

.store {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-m);
}
/* Two up before one up. Nine sponsorship tiers at one column is a very long
 * scroll on a tablet, and the inclusion lists stay readable at half width. */
@media (max-width: 68rem) {
  .store { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 44rem) {
  .store { grid-template-columns: 1fr; }
}

/* Column flex for the same reason .launcher uses it: the inclusion lists run
 * to wildly different lengths (nine deliverables at the top tier, three at
 * the bottom) and the actions have to land on one baseline per row anyway. */
.store-item {
  min-width: 0;
  max-width: none;                     /* the global li rule caps at --measure */
  display: flex;
  flex-direction: column;
  background: var(--block);
  border: var(--rule-hair) solid var(--rule-ink-load);
  padding: var(--space-m);
}

/* The head: name, then price. Name first in the DOM and in reading order,
 * because the price is meaningless until you know what it is the price of. */
.store-name {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: var(--track-1);
  line-height: var(--lh-heading);
  margin: 0;
  max-width: none;
}

/* The figure. --step-2 rather than --step-3: at 43px a five-digit
 * sponsorship price is the loudest thing on the page, which puts the money
 * above the deliverable and is exactly the emphasis these two pages exist to
 * avoid. 32px is unmissable without shouting. */
.store-price {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: var(--track-2);
  line-height: var(--lh-heading);
  font-variant-numeric: tabular-nums;
  margin-block-start: var(--space-2xs);
  max-width: none;
}
/* Currency rides at body size so it reads as a unit on the number rather
 * than as part of it. --fg-lo is de-emphasis, which is correct for a unit
 * and is NOT body copy, so the never-body rule is not being broken. */
.store-cur {
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: 0;
  color: var(--fg-lo);
}

/* The qualifier that travels with the price. ConvergX publishes an admin-fee
 * and tax line on every registration product and it sits HERE, directly under
 * the figure, never in a footnote. A delegate who reaches checkout and meets
 * a higher number than this page showed is the worst failure on the money
 * path. Verbatim, per product, never flattened into one blanket line. */
.store-qual {
  font-size: var(--step--1);
  color: var(--fg-lo);
  margin-block-start: var(--space-2xs);
  max-width: none;
  text-wrap: pretty;
}

.store-desc {
  margin-block-start: var(--space-s);
  font-size: var(--step--1);
  max-width: none;
  text-wrap: pretty;
}

/* The inclusions, as hairline-separated rows. This is the spec-sheet
 * treatment section 11 already uses for the module index, not a bulleted
 * list: a deliverable per line, scannable down the column, and it stays
 * legible when one tier carries nine and its neighbour carries three.
 * No checkmark glyphs. A tick is a persuasion device and it also implies a
 * comparison against some other tier's absence of one. */
.store-includes {
  list-style: none;
  padding: 0;
  margin-block-start: var(--space-m);
  border-top: var(--rule-hair) solid var(--rule-ink);
}
.store-includes > li {
  max-width: none;
  font-size: var(--step--1);
  line-height: var(--lh-body);
  padding-block: var(--space-2xs);
  border-bottom: var(--rule-hair) solid var(--rule-ink);
}

/* Pinned to the bottom so every action in a row shares one baseline however
 * long the list above it ran. Full-bleed inside the item's padding box: a
 * store's action is the width of the thing it buys. */
.store-act {
  margin-block-start: auto;
  padding-block-start: var(--space-m);
  max-width: none;
}
.store-act .btn {
  display: block;
  width: 100%;
  text-align: center;
}

/* The handoff line. Says where the money is actually taken, once per item,
 * at the point of the click rather than buried at the foot of the page. This
 * is the honesty condition on the whole storefront: the pages present the
 * products, and they never pretend to be the checkout. */
.store-away {
  font-size: var(--step--2);
  letter-spacing: 0;
  text-transform: none;
  color: var(--fg-lo);
  margin-block-start: var(--space-2xs);
  max-width: none;
}

/* Two side-by-side blocks. Added for the two hotel booking-step lists on
 * /congress/register/, which are siblings of equal weight and read badly
 * stacked: the second one looks like a continuation of the first. No
 * two-column utility existed and .two-path is a chooser with its own
 * dividers and hover, so it was the wrong thing to borrow. */
.pair {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-m);
}
@media (max-width: 44rem) {
  .pair { grid-template-columns: 1fr; }
}
.pair > * { min-width: 0; max-width: none; }
/* Numbered steps inside a block. The global ol rule caps at --measure,
 * which is wider than half the page, so the marker would sit outside the
 * block's padding without this. */
.pair ol { margin: var(--space-s) 0 0; padding-inline-start: var(--space-m); max-width: none; }
.pair ol li { max-width: none; font-size: var(--step--1); padding-block: var(--space-3xs); }

/* ---- the session disclosure, nested inside a day ---- */
/* WRITTEN FOR /congress/ #agenda AND NOW USED ON THE HOMEPAGE TOO, inside the
 * "What is ConvergX" primer, where three supports collapse to three openable
 * lines so the diagram is not pushed a screen down. Nothing had to change to
 * do that: every rule below is written unscoped (.sess > summary, never
 * #agenda .sess), so the component was already portable. It is the .faq rules
 * that carry #agenda overrides, which is the other half of why .faq was the
 * wrong pick for that job. KEEP THESE SELECTORS UNSCOPED.
 * On the homepage each one is wrapped in a .claim for the hairline and the
 * padding this deliberately does not carry; see the note there.
 *
 * The day-level .faq keeps its full treatment (a
 * 32px marker, generous padding, a hairline per row); a session sits INSIDE
 * a table cell one level down and cannot take any of that, or the timetable
 * turns into a stack of accordions.
 *
 * So it reads as a line of text that happens to open. No hairline (the table
 * already rules every row), no padding (the cell owns it), and the marker is
 * a small caret carried on the summary rather than the ::after plus sign the
 * day rows use, because at row size a plus reads as a bullet. */
.sess > summary {
  list-style: none;
  cursor: pointer;
  display: block;
  position: relative;
  padding-inline-end: var(--space-m);
}
.sess > summary::-webkit-details-marker { display: none; }
/* THE SAME +/- DEVICE THE DAY ROWS USE, at body size. A rotated angle
 * quotation mark was tried first and rendered as a hairline tick at this
 * size: too thin to read as a control, and nothing else on the site draws an
 * affordance that way. One device, two scales, is the right answer. */
.sess > summary::after {
  content: "+";
  position: absolute;
  inset-inline-end: 0;
  top: 0;
  color: var(--fg-lo);
  line-height: inherit;
  transition: color var(--t) linear;
}
.sess[open] > summary::after { content: "\2212"; color: var(--fg-hi); }
.sess > summary:hover,
.sess > summary:hover::after { color: var(--link); }
.sess > p {
  margin-block-start: var(--space-2xs);
  max-width: var(--measure);
  color: var(--fg-lo);
  font-size: var(--step--1);
}

/* A note that belongs to the whole row of products, sitting under it as a
 * direct child of the wrap. The unwrap rule gives it the page content box,
 * but .meta carries a --measure cap that wins against that width, and the
 * auto inline margins then CENTRE the short line instead of starting it on
 * the store's left edge. Measured at 450 against the store's 101. */
.store-note { max-width: none; }

/* ================= 41. THE FLOW ================= */
/* /congress/ only, between the impact statement and the subnav. Page comment
 * carries the brief and the history.
 *
 * ONE INDUSTRY AT A TIME, and this is the whole idea of the component.
 * Chip, 2026-07-31: "What I essentially want to show is aerospace and defense
 * lighting up at the same time as ConvergX and then from ConvergX, three nodes
 * light up. And then military lights up with ConvergX and connects to five
 * different spots, and then energy... What I want to show is one industry
 * problem at a time with ConvergX and that remains lit up while ConvergX makes
 * the connections in their network around the globe to those NATO-aligned
 * countries. So it should show that that's like an active relationship going
 * on at that time."
 * It used to be eight industries firing on scattered --d delays across one 16s
 * cycle, which read as a busy scatter rather than as one relationship. That is
 * gone. Do not put --d back on a sector.
 *
 * ONE CLOCK, AND IT IS NOW ONE SLOT LONG. --cycle is 6s and it is the length
 * of a SINGLE industry's turn, not the whole loop; the loop is eight slots,
 * 48s. Every keyframe percentage below is therefore a percentage of one
 * industry's relationship, which is what lets the four status lines sit on
 * four clean quarters of it.
 * WHY 6s AND NOT 4s. The four status lines have to be readable, and four lines
 * in a 4s slot is 1s each including the fade, which is under a comfortable
 * read. 6s gives each line 1.5s. Shorten the slot and the status track has to
 * lose lines.
 *
 * WHO DRIVES WHAT. _system/flow.js owns the SEQUENCE: which industry is up,
 * how many globe connections this slot gets (three to five, randomised), and
 * every piece of geometry that has to follow the turning globe. It expresses
 * all of that by toggling .is-live and rewriting path data, nothing else.
 * CSS owns the LOOK: the resting strokes, the glow, the travelling pulses, and
 * what lit means. The split matters because a keyframe cannot express "three
 * to five, chosen fresh each slot" and a script should not be re-implementing
 * a dash animation. If you find yourself adding a keyframe that needs to know
 * which industry is up, it belongs in flow.js instead.
 * --d survives on the FAN elements only, as a small stagger inside one slot.
 *
 * THE STAGE IS 1600 x 540, WIDE AND SHORT. Chip, 2026-07-31: "the graphic is
 * too TALL now but not wide enough." It was 1200 x 620, which at 1920 rendered
 * 1776 x 918 and stood taller than a laptop viewport. 1600 x 540 renders
 * 1776 x 600 at the same width. Eight sector rows in 540 is 67.5 units a row,
 * which is comfortable at every width the band renders at.
 * CHANGING THE VIEWBOX INVALIDATES EVERY NUMBER IN THE ART. The hub, the feed
 * origins, the fan, the output node, both node radii and the globe placement
 * below were all re-solved together for it. If you change it again, re-solve
 * all of them again; there is no partial edit that leaves this correct.
 *
 * THE GLOBE GEOMETRY IS SOLVED, NOT EYEBALLED, and this is the one thing in
 * the component that will silently break if edited carelessly. The plate is a
 * 1600x700 viewBox with its globe at cx 1130, cy 350, r 265. It is placed so
 * that globe lands at stage coordinates (1400, 270) with r 260:
 *     scale      = 260 / 265                   = 0.98113
 *     svg width  = 1600 * 0.98113 / 1600 stage = 98.113%
 *     svg left   = (1400 - 1130 * 0.98113) / 1600 =  18.208%
 *     svg top    = ( 270 -  350 * 0.98113) /  540 = -13.592%
 * That puts the disc at x 1140 to 1660 and y 10 to 530: it bleeds 60 units off
 * the right edge, as before, and clears the top and bottom by 10.
 * Every one of those is a PERCENTAGE OF THE STAGE, and the stage holds a fixed
 * aspect-ratio, so the whole figure scales as one thing and the globe lands at
 * the same stage coordinates at any stage width. That is what let the stage
 * get wider in July without re-solving anything.
 *
 * THE FAN NO LONGER TERMINATES ON SOLVED POINTS. It used to: eight endpoints
 * at 0.72r around that centre, computed in the generator, which is why the
 * numbers above once had to be edited in pairs. Since 2026-07-31 there are
 * three to five endpoints and _system/flow.js moves them every frame to follow
 * the turning globe, reading the registration back out of the DOM with
 * getScreenCTM instead of retyping the percentages. So these four numbers now
 * control only where the globe sits, and the lines follow it wherever it goes.
 * The markup still carries three solved points as the no-script fallback;
 * those ARE still tied to the numbers above.
 *
 * THE FEED ORIGINS ARE MEASURED, NOT TYPED, for the same reason. Chip wanted
 * each line to leave from the end of its own word, so the origin moves with
 * the label's length and the type scale. flow.js measures each row's dot and
 * writes the eight feed paths from that. The markup's eight are a snapshot
 * taken at 1280 for the no-script view; they will not match a different width
 * exactly, and they do not need to. */

.flow-band { padding-block: var(--space-2xl) var(--space-4xl); }

/* THE BAND'S OWN BLOCK-END PADDING IS FOR /congress/, where it is a direct
 * child of <main> and the thing after it is a sticky nav carrying no top
 * padding of its own, so this padding IS that join.
 * WHERE THE BAND CLOSES OUT A SURFACE, it must not spend it. Section 6b's law
 * is that a section owns the air ABOVE its content and nothing below, and every
 * other band on the site closes FLUSH on its last line and lets the next band's
 * section padding be the single interval. Keeping --space-4xl here would put
 * 288px of dead ground between the diagram and the muted band that follows it
 * on the homepage, which is a summed boundary and the thing that law exists to
 * prevent. On /congress/ the band is not a surface child, so this does not
 * match and that page is untouched.
 * ONE LINE, NOT ZERO, and the difference is that this band's last thing is a
 * BOX rather than a line of type. Every other band closes flush because a text
 * line box already carries descender space under its baseline, so flush still
 * reads as air. .flow-stage is a bare aspect-ratio box: measured at 1280, zero
 * put the ground boundary exactly on the stage edge with the status track 23px
 * off it. --space-m is the one-line step from section 6b, so the closing
 * interval stays commensurate and the next band's --space-4xl is still the
 * boundary doing the work. */
/* RAISED FROM --space-m TO --space-2xl, 2026-07-31. Chip: "There needs to be
 * more padding below the graphic." Measured at 1440 the old value left 27px
 * between the stage and the ground change, with the whole 163px interval
 * living on the far side of the seam, so the diagram read as jammed against
 * the bottom edge of its own band.
 * THE ANTI-SUMMING REASONING ABOVE STILL HOLDS and is why this is not
 * --space-4xl: that would have made 144 plus 163, a summed boundary. At
 * --space-2xl the closing interval matches the band's OWN top padding, so the
 * stage sits centred in its ground, and the total to the next heading comes to
 * 235px, which is one generous boundary rather than two stacked. */
[data-surface] > .flow-band:last-child { padding-block-end: var(--space-2xl); }

/* ---- THE COMPONENT ON A LIGHT GROUND ---- */
/* ONE VARIABLE, NOT A SECOND COPY OF THE COMPONENT. The band was drawn on the
 * dark ground and porting it to paper is a contrast problem, not a layout one.
 * --accent-lift (#3b89ff) is the DARK-surface blue: 5.30:1 on the ground and
 * 2.40:1 on paper, where it fails as text AND fails the 3:1 floor for a graphic
 * that carries meaning. It was doing four jobs here: the travelling pulse, the
 * lit sector dot, the lit node fill and the status track.
 *
 * --flow-ink is the one lit-graphic ink for the whole component. It defaults to
 * the dark value, so /congress/ renders byte-identically to before, and it is
 * redeclared on the two light grounds as --accent (#146ef5): 4.59:1 on paper
 * and 4.02:1 on the muted ground, both clear of the 3:1 graphic floor.
 *
 * THE STATUS TRACK IS NOT A GRAPHIC. It is 11px type, so it needs 4.5:1, and
 * there is already a token for exactly that job on every surface: --label-ink,
 * "the eyebrows", which resolves to --accent-lift on dark (5.30:1) and
 * --accent-text on light and muted (6.46:1 and 5.67:1). It gets that rather
 * than a second component variable, because inventing one would be a private
 * copy of a surface decision the token system already makes.
 *
 * SCOPED AS A DESCENDANT, which is how the rest of this file scopes a surface:
 * a section declares data-surface and everything inside inherits (tokens.css,
 * "the surface system"). The band never declares its own surface, so it reads
 * the one it is dropped into, and a third instance on a muted ground would need
 * no further edit. The alternative was .app-band's technique, a component
 * redeclaring the whole token set on itself, and that is the right pattern for
 * a band that PAINTS its own ground. This one does not paint anything. */
.flow-band { --flow-ink: var(--accent-lift); }
[data-surface="light"] .flow-band,
[data-surface="muted"] .flow-band { --flow-ink: var(--accent); }

/* ---- the stage stays inside the container ---- */
/* THE BAND HAS NO LAYOUT RULES OF ITS OWN. It is a plain .wrap like every
 * other section, so the stage's left and right edges land on the same content
 * edge as the section headings and the subnav. That is deliberate and it was
 * arrived at the hard way, so read the next paragraph before changing it.
 *
 * "WIDER" AND "INSIDE THE CONTAINER" ARE NOT IN CONFLICT, though the two notes
 * from Chip look like they are. First: "it could be a bit wider to take up more
 * space on the screen." That was answered by breaking the stage out of the wrap
 * with a full-width band and re-centred type, and the answer was wrong: "a bit
 * too wide now. It's outside of the main container."
 * What was actually wrong was the SHAPE, not the container. The stage was
 * 1200 x 620, which rendered 1776 x 918 at 1920 and read as a tall block. It is
 * now 1600 x 540 and renders about 1109 x 374 inside the container: shorter,
 * and proportionally far wider than anything before it. The aspect ratio
 * delivers the feeling of width; the breakout only delivered overhang.
 * SO DO NOT RE-ADD THE BREAKOUT. There is no --flow-edge, no full-width wrap
 * and no negative margin on the stage, and their absence is the fix. If the
 * band ever needs to feel wider again, change the viewBox and the aspect ratio
 * together, and re-solve the globe placement below with them. */

/* ---- the title row ---- */
/* Heading left, the sector sentence right, which is the shape Chip asked for.
 * The eight industries in that sentence are the only links in the component:
 * the diagram's own labels are plain text, because a label that lights up is
 * a state readout and making it a link as well gives one word two jobs. */
.flow-intro {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--space-l);
  /* TOP ALIGNED, Chip 2026-08-04. It was align-items: end, which was right
   * when the right column held one short sentence and the two blocks read as
   * a single line of type. With the admission standard added the column runs
   * to four lines, and bottom alignment pushed the heading down to meet its
   * last line, so the section appeared to start halfway down its own band.
   * start, not baseline: the site aligns adjacent prose columns on their
   * first baseline, but that rule assumes comparable type sizes. Against a
   * 53px heading it would drop the paragraph most of a line to meet the
   * heading baseline, which is the opposite of what was asked for. */
  align-items: start;
  margin-block-end: var(--space-2xl);
}
.flow-intro h2 {
  font-size: var(--step-4);
  letter-spacing: var(--track-4);
  color: var(--fg-hi);
  margin: 0;
}
.flow-intro-desc { max-width: none; color: var(--fg); margin: 0; }
/* BOTH PARAGRAPHS SHARE ONE GRID CELL, Chip 2026-08-04: put the admission
 * standard under the paragraph on the right, not under the title.
 * The wrapper is what makes that true at every width. Added as a bare second
 * <p>, the two-column grid auto-placed it into row 2 column 1, i.e. under the
 * heading. Forcing grid-column: 2 would have fixed the desktop and broken the
 * narrow layout, where the grid collapses to one column and there is no column
 * 2 to ask for. One cell holding both is width-independent. */
.flow-intro-body { min-width: 0; }
.flow-intro-desc + .flow-intro-desc { margin-block-start: var(--space-s); }

.flow { margin: 0; }

/* ---- the two column heads ---- */
/* A row above the stage rather than absolute pins on its corners. They carry
 * real copy now (an eyebrow in the nav's own language, then a sentence), and
 * a two line block hanging off a corner by a transform is a layout waiting to
 * collide with the first chip. */
.flow-heads {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2xl);
  margin-block-end: var(--space-xl);
}
.flow-head > .label { margin-block-end: var(--space-2xs); }
/* :not(.label) IS LOAD-BEARING. The eyebrow in this block is a <p class="label">,
 * so a bare .flow-head > p is (0,1,1) and outranks .label at (0,1,0): it was
 * repainting every eyebrow in --fg and they rendered grey on a site where
 * every eyebrow is blue. Chip caught it 2026-08-04. Do not simplify this
 * selector back. */
.flow-head > p:not(.label) { max-width: 42ch; font-size: var(--step--1); color: var(--fg); text-wrap: pretty; }
.flow-head--right { justify-self: end; text-align: end; }
.flow-head--right > p { margin-inline-start: auto; }

.flow-stage {
  /* One industry's turn. flow.js reads this to know how long a slot is, so the
   * script and the keyframes cannot drift apart; change it here only. */
  --cycle: 6s;
  /* How long an industry and the hub take to come up and go down. Long enough
   * to read as a relationship opening rather than a switch flipping, short
   * enough to be finished well inside the slot's first quarter. There is no
   * duration token in tokens.css, so this is component scoped next to --cycle
   * rather than invented as a global. */
  --lit-fade: 420ms;
  position: relative;
  display: grid;
  grid-template-columns: 30% 40% 30%;
  aspect-ratio: 1600 / 540;
}

/* ---- the globe, behind everything ---- */
/* Lifted from the homepage and reparented. It keeps data-globe, so globe.js
 * turns it and runs its own converging-network arcs with no work from here.
 * FADED RIGHT AND DOWN, per Chip. The mask uses currentColor rather than a
 * colour keyword deliberately: a mask reads the ALPHA channel and throws the
 * hue away, so currentColor is fully opaque and carries no palette decision.
 * Two ramps intersected, so the globe dissolves into the ground on both edges
 * instead of ending on a line.
 * THESE FOUR PERCENTAGES ARE COPIED INTO _system/flow.js. That file picks the
 * three sites the fan lands on, and it has to refuse any point where this mask
 * has taken the globe to nothing, or a connector ends in empty space. The art
 * layer is not masked, so it cannot work this out for itself. EDIT THE RAMPS
 * HERE AND EDIT MASK_X0/X1/Y0/Y1 THERE, in the same commit. There is a note on
 * the other side saying the same thing.
 * The right ramp moved from 42% to 61% when the stage went from 1200 to 1600
 * wide. That is not a taste change: both numbers put the fade at the same
 * distance from the globe measured in RADII (opaque until 1.62r left of
 * centre, gone by 0.54r right of it), which is what keeps the dissolve looking
 * identical at the new proportions. Solve it that way again if it moves. */
.flow-globe {
  /* What .fig-globe used to supply and the only thing worth keeping from it:
   * the plate's paths inherit this through currentColor. */
  color: var(--rule-ink-load);
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  -webkit-mask-image: linear-gradient(to right, currentColor 0 61%, transparent 96%),
                      linear-gradient(to bottom, currentColor 0 58%, transparent 100%);
          mask-image: linear-gradient(to right, currentColor 0 61%, transparent 96%),
                      linear-gradient(to bottom, currentColor 0 58%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* THE PLATE READS TOO FAINT ON PAPER, and this was looked at rather than
 * assumed. --rule-ink-load resolves to --cool-500 (#757575) on the dark ground
 * and --cool-400 (#898989) on light, which are 3.89:1 and 3.50:1: close on a
 * meter and not close to the eye, because a mid grey line gains against a
 * near-black surround and loses against paper. Rendered at 1280 the limb, the
 * land and the graticule all dissolved, and the plate stopped reading as a
 * globe behind the diagram.
 * ONE STEP UP THE SAME RAMP, to --cool-500 (4.76:1 on paper), which is the
 * value the dark ground is already using. It is a ramp token rather than one of
 * the surface set, and that is deliberate: this is a surface-specific
 * correction, which is the same job --flow-ink above is doing. The mask ramps
 * were the other candidate and they are NOT touched, because they are
 * transcribed into flow.js as MASK_X0/X1/Y0/Y1 and the two have to move
 * together; the fade was never the problem, the line tone was. */
[data-surface="light"] .flow-band .flow-globe,
[data-surface="muted"] .flow-band .flow-globe { color: var(--cool-500); }

.flow-globe svg {
  position: absolute;
  left: 18.208%;
  top: -13.592%;
  width: 98.113%;
  height: auto;
  /* THE RESET CLAMPS EVERY svg TO max-width: 100% (section 2, line 56) and it
   * silently capped this one at the stage width. Measured before the fix: the
   * globe rendered at r 171 where the fan paths expect 258, so every line
   * ended in empty space short of it. This plate is DELIBERATELY wider than
   * its box, because it bleeds off the right edge. */
  max-width: none;
}

.flow-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  z-index: 1;
}

/* ---- the resting network ---- */
.flow-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: calc(var(--rule-hair) * 2);
  opacity: 0.22;
}
.flow-node,
.flow-out {
  fill: var(--bg);
  stroke: var(--accent);
  stroke-width: calc(var(--rule-hair) * 2);
  opacity: 0.5;
}

/* ---- the travelling pulse ---- */
/* pathLength="100" is what lets one dash pattern work on a 240 unit feed line
 * and a 600 unit fan line: the dash is 14 percent of whatever the path is, so
 * the comet reads the same length everywhere.
 * THE GLOW IS ON THE PULSE, not on the line. Chip: "I wish that it was just
 * like glowing... almost like they had a matching blue outer glow when you're
 * lighting them up." drop-shadow on an SVG stroke is the only thing that gives
 * a real bloom; a second fatter stroke underneath reads as a fatter stroke. */
.flow-pulse {
  fill: none;
  stroke: var(--flow-ink);
  stroke-width: calc(var(--rule-hair) * 3);
  stroke-linecap: round;
  stroke-dasharray: 14 86;
  opacity: 0;
  filter: drop-shadow(0 0 6px var(--flow-ink));
}

/* ---- LIT: what an active relationship looks like ---- */
/* THE STATE IS A CLASS, NOT A KEYFRAME, and that is the load-bearing change.
 * The pair has to hold for a whole slot and the slot belongs to whichever
 * industry flow.js has up, which no keyframe can know. flow.js puts .is-live
 * on one .flow-sector and on .flow-hub for the same span, and takes it off
 * both at the same moment. A transition does the fade, so the handoff from one
 * industry to the next crosses over instead of snapping.
 * NOTHING ELSE MAY SET .is-live. It is the one signal that says which
 * relationship is open, and two writers would let two industries light. */
.flow-dot {
  inline-size: 0.5rem;
  block-size: 0.5rem;
  border-radius: 50%;
  flex: none;
  background: var(--rule-ink-load);
  transition: background var(--lit-fade) linear,
              box-shadow var(--lit-fade) linear,
              transform  var(--lit-fade) linear;
}
.flow-sector.is-live .flow-dot {
  background: var(--flow-ink);
  box-shadow: 0 0 14px 3px var(--flow-ink);
  transform: scale(1.7);
}
.flow-sector-name { transition: color var(--lit-fade) linear; }
.flow-sector.is-live .flow-sector-name { color: var(--fg-hi); }

/* The live row's own feed line lifts out of the resting set, so the eye can
 * see which of the eight is the one currently feeding the hub. Same stroke,
 * more of it; a second colour here would compete with the pulse. */
.flow-feed { transition: opacity var(--lit-fade) linear; }
.flow-feed.is-live { opacity: 0.6; }

/* ---- 1. a requirement is submitted ---- */
/* ONE pulse element, not eight. flow.js re-points it at the live row's
 * geometry each slot, which is why it carries no --d: there is only ever one
 * requirement travelling and it belongs to whichever industry is up. */
.flow-feed-pulse {
  animation: flow-feed-run var(--cycle) linear infinite;
}
@keyframes flow-feed-run {
  0%,  3%  { stroke-dashoffset: 100; opacity: 0; }
  5%       { opacity: 1; }
  15%      { opacity: 1; }
  17%      { stroke-dashoffset: 0;   opacity: 0; }
  100%     { stroke-dashoffset: 0;   opacity: 0; }
}

/* ---- 2. ConvergX asks the companies it has vetted ---- */
/* GLOW, NOT A RING. The expanding square outline that used to leave this box
 * is gone on Chip's instruction and must not come back. The hub blooms in
 * place: the border lifts to --flow-ink and a box-shadow opens and closes
 * around it. box-shadow spread is the whole effect and it costs no element. */
.flow-hub {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  max-width: none;
  z-index: 2;
  /* The hub's own ground hides every path end. Feed lines run to x=600 and fan
   * lines start there, dead centre under this box, so a pulse disappears as it
   * arrives and reappears as it leaves. That is why no path has to terminate
   * on a measured edge. */
  /* A FILLED BLUE BOX WITH A WHITE MARK, Chip's own suggestion 2026-07-31:
   * "I wonder if it would look better if it was blue with the logo in white."
   * It does, and it also fixes what he was actually reacting to. The outlined
   * version needed a large glow to read as lit at all, and a large glow on a
   * pale ground blooms. A filled box IS lit, so the glow drops to a hint.
   * IT STILL HIDES THE PATH ENDS, which is the load-bearing job of this
   * background: feed lines run to the hub centre and fan lines start there, so
   * a pulse disappears as it arrives and reappears as it leaves. An opaque
   * fill of any colour does that; a transparent one would break it.
   * One fill works on BOTH grounds, which an outline could not: white on
   * --accent measures 4.59:1 on dark and on light alike. */
  background: var(--accent);
  border: var(--rule-med) solid var(--accent);
  padding: var(--space-s) var(--space-m);
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: var(--track-1);
  line-height: 1;
  color: var(--accent-ink);
  white-space: nowrap;
  /* THE ONE ROUNDED CORNER IN THE PROJECT, on Chip's instruction 2026-07-31.
   * --radius stays 0 and is still marked structural: this is a local value on
   * one element, not a change to the token, so nothing else softens. Do not
   * promote it to --radius, and do not copy it onto another component without
   * asking. */
  border-radius: 10px;
  /* Lit by class, for exactly as long as its industry is. See the LIT block
   * above; the keyframe that used to run this on the cycle is gone, because
   * the hub now has to hold for a whole slot and release with its partner. */
  transition: border-color var(--lit-fade) linear,
              box-shadow   var(--lit-fade) linear;
}
/* THE LIT STATE IS NOW A HINT, NOT A FLARE. Chip: "the glow on the convergex
 * logo is TOO bright." It was 44px of blur and 6px of spread, sized for an
 * outlined box that had nothing else to signal with. The box is filled now, so
 * the glow only has to say "this one is active", and a filled blue box on
 * paper reads that at a fraction of the intensity. Spread comes off entirely:
 * spread on a pale ground paints a hard second edge around the box. */
.flow-hub.is-live {
  box-shadow: 0 0 20px 0 var(--accent);
}
/* The mark, swapped in by shell.js. --accent-ink, not --fg-hi: the box is a
 * blue fill now, so the mark reverses out of it on both grounds and needs no
 * separate reversed asset. Measured 4.59:1. */
.flow-hub .logo-art {
  display: block;
  inline-size: clamp(7rem, 11vw, 9.5rem);
  block-size: auto;
  color: var(--accent-ink);
  /* fill AS WELL AS color. The source paths carry no fill attribute, so they
   * take the SVG default of black unless something says otherwise; the header
   * lockup gets its fill from its own rule, which does not reach here. Setting
   * color alone left the mark rendering black on a near-black ground. */
  fill: currentColor;
}
/* ---- 3. vetted companies answer ---- */
/* --d survives HERE and only here: a small stagger so the three to five
 * connections leave the hub in quick succession rather than as one thick
 * flash. It is a few hundredths of a second, well inside the beat. */
.flow-fan-pulse {
  animation: flow-fan-run var(--cycle) linear infinite;
  animation-delay: var(--d);
}
@keyframes flow-fan-run {
  0%, 26%  { stroke-dashoffset: 100; opacity: 0; }
  28%      { opacity: 1; }
  41%      { opacity: 1; }
  43%      { stroke-dashoffset: 0;   opacity: 0; }
  100%     { stroke-dashoffset: 0;   opacity: 0; }
}
.flow-node {
  animation: flow-node-run var(--cycle) linear infinite;
  animation-delay: var(--d);
}
/* Lit from the moment the outbound pulse lands until the introduction leaves,
 * which is most of beats three and four. These are the companies answering, so
 * they stay up while that is what the status line says. */
@keyframes flow-node-run {
  0%, 41%   { fill: var(--bg);        opacity: 0.5; filter: none; }
  45%       { fill: var(--flow-ink);  opacity: 1;   filter: drop-shadow(0 0 8px var(--flow-ink)); }
  78%       { fill: var(--flow-ink);  opacity: 1;   filter: drop-shadow(0 0 8px var(--flow-ink)); }
  84%, 100% { fill: var(--bg);        opacity: 0.5; filter: none; }
}
/* The same paths run backwards. dashoffset 0 to 100 instead of 100 to 0 is the
 * entire difference, which is why the return needed no second geometry. */
.flow-return-pulse {
  animation: flow-return-run var(--cycle) linear infinite;
  animation-delay: var(--d);
}
@keyframes flow-return-run {
  0%, 54%  { stroke-dashoffset: 0;   opacity: 0; }
  56%      { opacity: 1; }
  68%      { opacity: 1; }
  70%      { stroke-dashoffset: 100; opacity: 0; }
  100%     { stroke-dashoffset: 100; opacity: 0; }
}

/* ---- 4. one introduction is brokered ---- */
/* ONE STRAIGHT DROP, not two strands meeting. Chip, 2026-07-31: "Instead of
 * the U shape here, just do a straight line to the result that ConvergX is
 * spitting out." Two curves used to swing out to x 470 and x 730 and meet
 * under the hub, which read as two more things arriving rather than as one
 * result leaving. The second path is deleted, not hidden. Nothing here changed
 * with it: same class, same timing, same node. */
.flow-conv-pulse {
  animation: flow-conv-run var(--cycle) linear infinite;
  animation-delay: var(--d);
}
@keyframes flow-conv-run {
  0%, 76%  { stroke-dashoffset: 100; opacity: 0; }
  78%      { opacity: 1; }
  86%      { opacity: 1; }
  88%      { stroke-dashoffset: 0;   opacity: 0; }
  100%     { stroke-dashoffset: 0;   opacity: 0; }
}
.flow-out { animation: flow-out-run var(--cycle) linear infinite; }
/* Back to resting BEFORE the slot ends, so the next industry does not inherit
 * a lit output node from the last one's introduction. */
@keyframes flow-out-run {
  0%, 84%   { fill: var(--bg);        opacity: 0.5; filter: none; }
  88%       { fill: var(--flow-ink);  opacity: 1;   filter: drop-shadow(0 0 12px var(--flow-ink)); }
  96%       { fill: var(--flow-ink);  opacity: 1;   filter: drop-shadow(0 0 12px var(--flow-ink)); }
  99%, 100% { fill: var(--bg);        opacity: 0.5; filter: none; }
}

/* ---- the status track ---- */
/* Four lines in ONE grid cell, so the box is the height of the tallest and the
 * layout never jumps as they swap. Each fades in over the beat it names.
 * --s is the beat number, 1 to 4, and the delay is derived from it rather than
 * typed per line: one quarter of the cycle each, offset back by the length of
 * the fade-in so a line is fully up when its beat starts. */
.flow-status {
  position: absolute;
  left: 50%;
  top: 92%;
  transform: translate(-50%, -50%);
  margin: 0;
  max-width: none;
  z-index: 2;
  display: grid;
  place-items: center;
  text-align: center;
}
.flow-status > span {
  grid-area: 1 / 1;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
  line-height: var(--lh-label);
  /* 11px type, so this is the 4.5:1 job and not the 3:1 one. --label-ink is
   * the token for exactly that on every surface: --accent-lift on dark
   * (5.30:1) and --accent-text on light and muted (6.46:1 and 5.67:1). It is
   * NOT --flow-ink, which is the graphic ink and lands at 4.59:1 on paper. */
  color: var(--label-ink);
  opacity: 0;
  animation: flow-status-run var(--cycle) linear infinite;
  animation-delay: calc((var(--s) - 1) * var(--cycle) / 4 + var(--cycle) * 0.02);
}
@keyframes flow-status-run {
  0%      { opacity: 0; }
  3%, 22% { opacity: 1; }
  25%     { opacity: 0; }
  100%    { opacity: 0; }
}

/* ---- the sectors column ---- */
/* TEN equal rows is not a style choice, it is the registration contract:
 * chip i is centred at (i + 0.5)/10 of the stage height and the ten feed
 * paths leave at exactly those ten numbers. It was eight until 2026-08-13,
 * when the client's approved vertical list replaced them. Change the count
 * and FOUR things move together: this repeat(), the <li>s, the .flow-feed
 * paths beside them, and ROWS plus ORDER in flow.js. */
.flow-sectors {
  grid-column: 1;
  grid-row: 1;
  z-index: 2;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-rows: repeat(10, 1fr);
}
/* THE DOT IS THE ORIGIN, and that is now literal: flow.js measures each dot's
 * centre off the DOM and starts that row's feed path exactly there. Nothing
 * about the feed geometry is typed any more, which is what lets the labels sit
 * wherever the type scale puts them.
 *
 * LEFT ALIGNED. Chip, 2026-07-31: "The industries on the left just seemed far
 * away. They could almost be left-aligned and the nodes going to the
 * industries could be a bit more random where they connect to them, at the end
 * of the word." So the rows start at the column's left edge and each line
 * leaves from the end of its own word, which means the eight origins are
 * ragged by construction. That raggedness IS the randomness he asked for; it
 * is not decoration added on top.
 * It was right aligned for one day, when the column was a fixed 30% and the
 * feed paths were typed at a single x. Both of those are gone.
 *
 * --g is the gap between a word and its dot, set per row in the markup. It
 * varies the origins a little further so the eight do not resolve into a neat
 * diagonal following label length alone. Small values only: the dot has to
 * still read as belonging to that word. */
.flow-sector {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--g, var(--space-s));
  max-width: none;
  min-width: 0;
}
.flow-sector-name { font-size: var(--step--1); line-height: var(--lh-label); color: var(--fg-lo); }

/* ---- reduced motion ---- */
/* EVERY animation stops and the diagram is left in ONE HELD MOMENT, the same
 * one the no-script view shows: the first industry and the hub lit together,
 * their connections drawn to real points on the globe, nothing travelling.
 * That is a legible still of exactly what the component is about, which a bare
 * unlit network would not have been.
 * The page-level reduce block zeroes duration, which is undefined behaviour on
 * an infinite loop rather than a stop, so it is named explicitly here.
 * TRANSITIONS TOO, not just animations. .is-live is a transition now, and a
 * running transition is motion whatever it is attached to. The lit state still
 * applies, it just arrives instantly.
 * flow.js holds up its end: it stops advancing the slot under the same query,
 * so nothing ever removes .is-live from the industry the markup shipped lit.
 * The globe stops as well; globe.js checks the same query itself. */
@media (prefers-reduced-motion: reduce) {
  .flow-stage *,
  .flow-stage *::before,
  .flow-stage *::after { animation: none !important; transition: none !important; }
  .flow-pulse    { opacity: 0; }
  .flow-status > span:not([style*="--s:1"]) { opacity: 0; }
  .flow-status > span[style*="--s:1"] { opacity: 1; }
}

/* ---- narrow ---- */
/* THE WHOLE BAND COMES OFF. Chip, 2026-07-31: "Probably don't need to see any
 * of this on the smaller mobile sizes, just desktop."
 *
 * IT WAS A DEGRADED FALLBACK AND THAT IS GONE TOO. The art and the globe came
 * off and the eight labels wrapped into a list under a static hub and a static
 * status line, which was three animation beats' worth of markup rendering as a
 * plain list of industries. Those rules are deleted rather than left orphaned;
 * recoverable from git.
 *
 * NO CONTENT IS LOST ON A PHONE, and this was checked before it was agreed.
 * The band's only unique claim is the answer to "who attends", and the FAQ
 * further down this page carries "Who attends the Congress?" as its own
 * question with the same answer in prose. The eight industries are also links
 * in the intro sentence, which is inside this band, but every one of them is
 * reachable from the industries index in the footer and the subnav. So this is
 * a decoration that stops rendering, not a section that disappears. Do not
 * restore the band for a phone thinking a reader lost something. */
@media (max-width: 60rem) {
  .flow-band { display: none; }
}

/* ================= 42. THE CONGRESS APP BAND ================= */
/* /congress/ #app only. Chip, 2026-07-31: "make that a full blue background
 * with the screenshot on the right and the info on the left."
 *
 * THE TOKENS ARE REMAPPED, NOTHING IS HAND-COLOURED. Same technique as the
 * three data-surface blocks: the band redeclares the component variables and
 * every child (.sec-head, .link-more, .mod-shot-frame) keeps working with no
 * knowledge of where it is. Hand-painting each child would have left the next
 * component dropped in here rendering dark-on-blue.
 *
 * ONE INK, AND IT IS NOT A SHORTCUT. --fg, --fg-hi and --fg-lo all resolve to
 * --accent-ink here, which measures 4.59:1 on --accent. There is no dimmer
 * white available: anything stepped down from #ffffff on this blue falls under
 * 4.5:1 and stops being legible body copy. So the band has no de-emphasis
 * layer, which is why nothing inside it uses a caption or a muted note. Do not
 * introduce one by adding a --fg-lo shade, and do not put an 11px .label in
 * here for the same reason.
 *
 * THE PAGE'S ONE FULL-BLEED ACCENT, and it is the last band before the footer.
 * That position is deliberate: an accent field this size anywhere above the
 * hero photograph would compete with it. */
/* CHROME AS OF 2026-08-05, was the accent. The remap below is the whole edit:
 * the band re-points the surface tokens and every component inside it follows,
 * which is why nothing in this section hand-writes a colour.
 * ALL SIX INKS FLIPPED WITH THE GROUND. Blue is dark and took white type;
 * chrome is mid and takes dark type. Leaving --block or the rules on the old
 * accent-ink would have put white lines on a mid-grey field at 3.26:1.
 * --block is --warm here rather than --chrome-ink: a panel inside a chrome band
 * needs to separate from it, and warm at 1.28:1 against paper reads as a panel
 * without introducing a third grey. */
.app-band {
  --bg:            var(--chrome);
  --fg:            var(--chrome-ink);
  --fg-hi:         var(--chrome-ink);
  --fg-lo:         var(--chrome-ink);
  --block:         var(--warm);
  --rule-ink:      var(--chrome-ink);
  --rule-ink-load: var(--chrome-ink);
  --link:          var(--chrome-ink);
  --label-ink:     var(--chrome-ink);
  background: var(--bg);
  color: var(--fg);
  padding-block: var(--space-4xl);
}
/* The heading joins the four-title set. Restated rather than added to that
 * selector list because those four sit on the page ground and this one does
 * not, so if the group ever gains a colour this one must not inherit it. */
.app-band .sec-head h2 {
  font-size: var(--step-4);
  letter-spacing: var(--track-4);
}
.app-band .sec-head {
  border-top: 0;
  padding-block-start: 0;
}

/* Copy left, screenshot right. Same 5/6 split .mod-feature-inner uses, and
 * for the same reason: a phone screenshot wants the wider half. */
.app-inner {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: var(--space-2xl);
  align-items: start;
  margin-block-start: var(--space-xl);
}
@media (max-width: 60rem) {
  .app-inner { grid-template-columns: 1fr; gap: var(--space-xl); }
}

/* The subhead Chip asked for: what the old h3 said, one rung under the h2 and
 * above the description rather than beside it. A <p>, not a heading, because
 * the section already has its heading and a second one would put a rank in
 * the outline that the content does not have. */
.app-sub {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: var(--track-2);
  line-height: var(--lh-heading);
  max-width: 22ch;
  margin: 0 0 var(--space-m);
}
.app-copy p { max-width: var(--measure); }

/* The empty screenshot frame inherits --rule-ink-load, which is white in this
 * band, so its border would have been a solid white box. Held back to the
 * hairline weight it has everywhere else and let it read as a placeholder. */
.app-band .mod-shot-frame { border-color: var(--chrome-ink); }

/* ---- the subnav's page-link group ---- */
/* Everything left of this moves you down the page, everything right of it
 * leaves the page. auto margin on the first of the two opens the gap; there
 * is no separator glyph, because the gap IS the separator.
 * The two are real links to real pages and carry no aria-current: wireSubnav()
 * collects a[href^="#"] only, so they are outside the marker by construction.
 * On a narrow screen the bar is already a horizontal scroller, and an auto
 * margin inside an overflowing flex row resolves to zero, so the group simply
 * falls in at the end of the run. That is correct and needs no query. */
.subnav-list > li.subnav-end { margin-inline-start: auto; }

/* ================= 43. THE HOMEPAGE PRIMER ================= */
/* index.html, the "What is ConvergX" section that introduces the flow band
 * directly under it. Three changes Chip asked for on 2026-07-31, all of them
 * about this section reading as one thing with the diagram rather than as a
 * generic section that happens to sit above it. */

/* NO RULE ABOVE THE HEADING. Every other .sec-head on the site carries a
 * hairline that separates it from the section before it. This one opens a
 * band on a new ground, so the ground change IS the separation and the rule
 * was drawing a line inside a boundary that already existed. */
[aria-labelledby="whatcx-h"] .sec-head {
  border-top: 0;
  padding-block-start: 0;
}

/* THE SAME SIZE AS THE CONGRESS PAGE TITLES. Chip: "Make What is ConvergX
 * bigger, like the titles on the Conference page." That is --step-4 with
 * --track-4, the size Congress Overview, The Speakers, The Agenda, Questions
 * and The Congress App all share. Stated here rather than added to that
 * selector list because those four are scoped to /congress/ ids. */
[aria-labelledby="whatcx-h"] .sec-head h2 {
  font-size: var(--step-4);
  letter-spacing: var(--track-4);
}

/* The core message, moved out of the right rail so only the three disclosures
 * live there. Body size, not the lede's --step-1: it follows a lede, and
 * repeating that scale would turn the whole left column into one
 * undifferentiated block with no way in. */
.whatcx-say {
  margin-block-start: var(--space-m);
  max-width: var(--measure);
  color: var(--fg);
}
.whatcx-say:first-of-type { margin-block-start: var(--space-l); }

/* AIR UNDER THE DIAGRAM is handled at the :last-child rule in section 41, not
 * here. A surface-scoped rule was written at this point first and never won:
 * [data-surface] > .flow-band:last-child is (0,3,0) against its (0,2,0), so
 * the value that actually governs is that one. Fixing it at the source beats
 * escalating specificity to beat a rule that was already correct in shape. */

/* ================= 45. THE VEILED PHOTOGRAPHIC HERO ================= */
/* The eight sector pages. Opt in per page with [data-hero="veil"] on <body>
 * plus .hero-veil-band on the section, exactly the way section 30 opts in.
 *
 * WHY THIS IS NOT .hero-photo-band, WHICH IS THE FIRST QUESTION ANYONE WILL
 * ASK. Both components put a photograph behind hero copy and there the
 * agreement ends. They are siblings, not one component with a variant, and
 * folding them together would mean a rule that immediately has to fork:
 *
 *   TREATMENT   /congress/ is a DUOTONE. Section 30 flattens the frame to
 *               luminance, pushes a blue through it on a `color` blend and
 *               rebuilds the saturation. The sector heroes take NO filter and
 *               NO tint: the photograph keeps its own colour and a neutral
 *               dark veil sits over it. This is the whole brief, and it is the
 *               opposite instruction, not a parameter.
 *   SCRIM AXIS  /congress/ washes LEFT TO RIGHT, heaviest under a copy column
 *               that ends around 79% across, clearing to 0.14 so the room
 *               stays visible. This one washes TOP DOWN, heaviest at the top
 *               where the sticky bar sits, clearing toward the bottom.
 *               A gradient direction is not a value you tune, it is a
 *               different composition.
 *   BOTTOM EDGE /congress/ resolves its frame into --bg with a second
 *               gradient, because the photograph and the page below it share a
 *               ground and any seam would read as a rendering fault. The
 *               sector band is a DARK band on a LIGHT page: its bottom edge is
 *               a deliberate surface change that has been there since the band
 *               was two columns, so there is nothing to fade into and section
 *               45 has no equivalent of that layer.
 *   POPULATION  one page against eight. A shared rule would carry a
 *               [data-hero="photo"] exception in almost every declaration.
 *
 * WHAT THEY DO SHARE IS SHARED PROPERLY, in one place each, rather than by
 * copy: the header-transparency behaviour is chrome and lives in section 30
 * with both attributes on its selectors, and the vertical footprint is
 * --hero-photo-h, declared once in section 31 and read by both. Those are the
 * two things that genuinely must not drift. Everything else must.
 *
 * WHAT IT REPLACED. Section 21's .hero-vert: type in a seven-column track,
 * photograph in a five-column track beside it, one accent rule on the seam.
 * That component is NOT deleted and is still live, because /industries/
 * manufacturing/ and /industries/military/ render it with the copy column and
 * no photograph while they wait for theirs to be shot.
 *
 * PROGRESSIVE ENHANCEMENT. Nothing here needs JavaScript. The photograph is an
 * <img> in the markup, the veil is a <span> painted by CSS, and the layer sits
 * at top: 0 by default so that with scripts off, where shell.js never runs and
 * the <noscript> nav renders in normal flow, the frame starts at the top of
 * the band instead of being dragged up over the bottom of that nav list. The
 * climb behind the bar is gated on :has(.shell-header), a class only the real
 * shell adds. Same mechanism and same reasoning as section 30. */

.hero-veil-band {
  position: relative;
  isolation: isolate;   /* the veil's stacking context stops at the band */
  padding-block: 0;     /* the band's edges are the photograph's edges */
}

/* THE PHOTOGRAPH LAYER. Pulled up behind the nav bar by exactly one bar
 * height, and only once the real shell exists. See the no-JS note above: the
 * zero is the fallback, not an oversight. */
/* z-index 0, NOT the -1 section 30 uses, and the difference is the section's
 * own background. THIS COST A RENDER AND IS WORTH WRITING DOWN. /congress/
 * carries data-surface on the <body>, so .hero-photo-band paints no ground of
 * its own and a layer at -1 shows through it to the page behind. These sector
 * bands carry data-surface="dark" on the SECTION, because the page around them
 * is light and only the band inverts, so the section DOES paint --ground. A
 * child at -1 paints behind its own parent's background and the photograph
 * disappeared completely: a flat dark band, no error, nothing in the console.
 *
 * The attribute has to stay where it is. It is what resolves --ink, --fg-hi
 * and the rest for every piece of copy in the band, and moving it would relight
 * the whole page. So the layer climbs above the background instead of hiding
 * under it, and .hero-veil-band > .editorial takes an explicit layer above
 * that. Both are stated rather than left to source order, because "it works
 * because the markup happens to be in this order" is the next silent break. */
[data-hero="veil"] .hero-veil {
  position: absolute;
  inset-inline: 0;
  top: 0;
  bottom: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.hero-veil-band > .editorial { position: relative; z-index: 1; }
[data-hero="veil"]:has(.shell-header) .hero-veil {
  top: calc(-1 * var(--shell-h));
}

/* NO FILTER, AND THAT IS THE COMPONENT. Section 30's img rule carries
 * grayscale, contrast, brightness and saturate to build the duotone. There is
 * deliberately no filter property here at all: the frames are colour
 * photographs and they stay colour photographs. If anyone ever adds a
 * brightness() here to "help the text", the veil below is the thing to deepen
 * instead, because that is the layer whose contribution is measurable.
 *
 * NO object-position EITHER, AND THAT IS LOAD-BEARING. Every image in this
 * component carries a .focal--* class, and those rules are specificity 0,1,0.
 * A default stated here would be 0,2,1 and would BEAT all four of them,
 * silently discarding the art direction the classes exist to carry. The
 * initial value for object-fit: cover is a centre crop, which is a sane
 * fallback for any frame that arrives without a focal class. See the section
 * 21 comment for what each focal value is protecting and why they need
 * re-deriving against this wide crop once the new files land. */
[data-hero="veil"] .hero-veil img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* THE VEIL. A SEPARATE ELEMENT, not a pseudo, and not because a pseudo could
 * not paint it. It is separate because it is the thing that has to be tuned:
 * eight photographs will pass under it, they are not equally dark, and a layer
 * you can point at in the markup is a layer somebody can reason about and
 * measure. Chip asked for it as its own object and he is right.
 *
 * TOP DOWN, HEAVIEST AT THE TOP:
 *   0%    0.86. The sticky header sits here. This stop is a UI legibility
 *         floor for nav labels over an arbitrary frame, not a text floor,
 *         and it is the heaviest number in the gradient for that reason.
 *   45%   0.80. Still carrying most of its weight. The eyebrow and the whole
 *         --step-6 headline land above this, and a gradient that has already
 *         started clearing under the headline is a gradient that fails on
 *         whichever of the eight photographs happens to be bright there.
 *   100%  0.58. The clear-off, and it is SHALLOWER THAN IT LOOKS. See below.
 *
 * WHY THE FADE DOES NOT RUN TO ZERO, WHICH IS THE ONE THING ABOUT THIS
 * COMPONENT THAT LOOKS LIKE A COMPROMISE AND IS NOT. The first version ran
 * 0.90 down to 0.26, on the assumption that the copy occupied the top two
 * thirds of the band and the bottom third was free photograph. MEASURED, that
 * assumption is false, and false structurally rather than by accident. The
 * band has no authored height: it is --hero-offset plus the copy plus
 * --space-2xl, so the copy ALWAYS ends about one --space-2xl above the bottom
 * edge, whatever the page and whatever the viewport. On /industries/energy/ at
 * 1280 the last lede runs from 82.3% to 92.6% of the veil box. There is no
 * free bottom third to fade into on any of these pages, and against the
 * photographs in place at the time the deep version measured that lede at
 * 1.70:1, which is not text.
 *
 * WHAT WAS REJECTED, so it is not re-proposed: padding the band out at the
 * bottom to manufacture a fade zone. That buys the gradient room by inventing
 * height the design did not ask for and pushes the band past the Congress
 * footprint it is supposed to match. Section 31 makes the same call in the
 * other direction and is worth reading: starving the copy to fit a gradient is
 * the wrong half of that trade. The gradient serves the copy.
 *
 * THESE THREE NUMBERS ARE MEASURED, NOT CHOSEN. Re-measured 2026-08-03 against
 * the current photographs, all eight pages, at 1280 and 1920, sampling the
 * real composited pixels behind each text element rather than an average of
 * the frame. Reported figure is the WORST SINGLE PIXEL under the eyebrow, the
 * h1, the lede, the nav and the wordmark, which is the strictest reading
 * available and the only one that survives a photograph with a highlight in it.
 *
 *   veil            worst case, all 8 pages, 1280 and 1920
 *   0.90/0.87/0.70  8.41:1
 *   0.86/0.80/0.58  6.89:1   <- shipped
 *   0.82/0.74/0.50  5.99:1
 *   0.78/0.68/0.42  5.21:1
 *
 * /industries/construction/ at 1920 is the binding page in every row, and
 * /industries/agriculture/ is within a tenth of it. Both put open overcast sky
 * across the top of the frame, directly under the header. Every row above
 * clears the 4.5:1 text floor and the 3:1 UI floor, so the choice was not
 * legibility against failure, it was how much photograph to give back.
 *
 * WHY NOT THE LIGHTEST ROW, since it also passes. Two reasons and the second
 * is the real one. It leaves a 16 percent margin over the floor, and a margin
 * that thin is one photograph away from being wrong. And every one of these
 * eight files is a GENERATED PLACEHOLDER: the commissioned frames have not
 * been shot, their tonality is unknown, and a veil tuned to the exact edge of
 * the stand-ins would have to be re-tuned the day the real ones land. 0.86 is
 * the setting that gives the photograph back without spending the headroom
 * that absorbs the next image.
 *
 * RE-MEASURE WHEN THE COMMISSIONED FRAMES LAND. Start with
 * /industries/construction/ at 1920, then /industries/agriculture/. If either
 * fails, deepen this gradient rather than filtering the image: the veil is the
 * layer whose contribution is measurable.
 *
 * TOKEN-DERIVED, NOT A LITERAL, and this is the one place it differs from
 * section 30 on purpose. That section wrote rgba() literals and said plainly
 * why: a wash compositing against image pixels cannot resolve through a
 * surface token, because there is no surface under it to resolve against.
 * That reasoning is about which GROUND the wash sits on and it is correct.
 * It is not a reason the wash's own colour has to be a hand-typed number.
 * color-mix against --ground gives the identical near-black neutral, keeps the
 * value traceable to the palette, and means a palette change carries here
 * instead of leaving a stale hard-coded grey behind. --ground is the dark grey
 * ground rather than a true black, which is what keeps this reading as a veil
 * over a photograph and not as a black sheet.
 * SUPPORT: color-mix is the same generation as the :has() this file already
 * depends on in section 30 and above, so it introduces no new floor.
 *
 * NEUTRAL, AND THAT IS THE POINT. No hue, no blend mode, no tint. The
 * photograph's own colour survives underneath. Anything that shifts hue here
 * is the Congress treatment arriving by the back door. */
[data-hero="veil"] .hero-veil > span {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--ground) 86%, transparent) 0%,
    color-mix(in srgb, var(--ground) 80%, transparent) 45%,
    color-mix(in srgb, var(--ground) 58%, transparent) 100%);
}

/* THE FOOTPRINT, and it is read, never retyped. --hero-photo-h is declared in
 * section 31 next to the Congress hero it was extracted from, so the two
 * heroes occupy the same vertical space by construction rather than by
 * agreement. The bottom pad matches too: the copy is top-aligned and the slack
 * falls below it, so without a pad the last line of the lede would sit against
 * the band's bottom edge on the pages whose copy runs short. */
.hero-veil-band > .editorial {
  min-height: var(--hero-photo-h);
  padding-block: var(--hero-offset) var(--space-2xl);
}

/* THE COPY COLUMN. Eight of twelve rather than section 21's seven: the
 * photograph no longer occupies a track of its own, so the copy is not being
 * held off anything, and a --step-6 headline in a narrow track is what was
 * driving the old band to four lines. It stays a COLUMN rather than going full
 * width because a headline that runs the whole page has no shape, and because
 * the right third of the frame is where the photograph is still legible under
 * the veil. Every declaration below is section 21's .hero-vert-copy, restated
 * rather than inherited: the two components must be able to move apart. */
.hero-veil-copy {
  grid-column: main-start / span 8;
  align-self: start;
  display: grid;
  justify-items: start;
  row-gap: var(--gap-under-heading);
}
/* Same law as every other hero: the h1 owns its lede, and the label above it
 * is a separate idea and gets the wider interval. */
.hero-veil-copy h1 { margin-block-start: var(--space-xl); }
/* The eyebrow wrapper is a bare <header> holding one inline label. Section 26
 * already matches `header:has(> .label:only-child)` and gives it the label's
 * own leading, so the eyebrow lands on --hero-offset here like everywhere
 * else. Nothing needs restating for it; this note exists so the next person
 * does not add a rule that is already being applied. */

/* THE EYEBROW INK, AND IT HAS TO MOVE. On the dark ground --label-ink is
 * --accent-lift, a blue that measures 5.30:1 against that ground. Over a
 * VEILED PHOTOGRAPH there is no ground: the blue composites against whatever
 * the frame is doing, and on a frame lit warm and orange, which every one of
 * these specs asks for, blue-on-warm at 11px is the first thing to fail.
 * Lifted to the emphasis ink and let the veil carry it, which is exactly the
 * call section 30 made for the same reason on the Congress eyebrow. Scoped to
 * this hero alone; every other eyebrow on all 64 pages is untouched. */
[data-hero="veil"] .hero-veil-copy .label { color: var(--ink-hi); }

/* NARROW WIDTHS. The band keeps its full-bleed photograph, unlike section 21,
 * which dropped its image below the copy at 16:10. There is nothing to drop
 * here: the photograph IS the ground, and a ground does not reflow.
 *
 * THE COLUMN HAS TO BE RESTATED OR IT OVERFLOWS. .hero-veil-copy is
 * `main-start / span 8` at specificity 0,1,0, and there is no lower-specificity
 * collapse rule that beats it, so at 375px the span-8 placement would survive
 * into a grid whose content columns are far narrower and the copy would run
 * off the edge. Section 31 records the same trap being hit and caught by
 * measuring scrollWidth rather than by reading the rule. Restated here.
 *
 * THE VEIL GOES FLAT. A top-down wash still works on a phone, but the copy
 * occupies proportionally far more of a short band, so the stretch that could
 * afford to clear no longer exists. This keeps a single weight across the whole
 * frame. It is stated at the SAME specificity as the base rule and sits later
 * in the file, so it wins; it is inside a max-width query, so it cannot leak up
 * to desktop.
 *
 * 0.74 IS MEASURED, and it is close to the desktop gradient's own average
 * (0.86, 0.80 and 0.58 average to about 0.75), which is what a flat stand-in
 * for that gradient should be. Sampled 2026-08-03 at 375 on the two binding
 * pages, worst single pixel under any text:
 *   flat 0.82  9.60:1     flat 0.74  8.29:1   <- shipped
 *   flat 0.78  8.93:1     flat 0.70  7.68:1
 * Narrow widths run FAR safer than desktop, and the reason is the crop, not
 * luck: at 375 the box is tall and narrow, so cover takes a centre slice of a
 * 16:9 frame and most of the open sky that makes /industries/construction/ and
 * /industries/agriculture/ hard at 1920 is cropped away. Do not assume that
 * relationship holds for a differently composed photograph. Re-measure at both
 * ends when the commissioned frames land. */
@media (max-width: 60rem) {
  .hero-veil-copy { grid-column: main; }
  .hero-veil-band > .editorial { padding-block: var(--hero-offset) var(--space-xl); }
  [data-hero="veil"] .hero-veil > span {
    background: color-mix(in srgb, var(--ground) 74%, transparent);
  }
}

/* ---- a band that closes the page behind fixed-position siblings -----------
 * /about/ only, in practice. Added 2026-08-03 when Leadership merged into it.
 *
 * THE DEFECT. A closing band normally gets its bottom padding from a
 * :last-child rule. On /about/ the muted two-path band IS the last thing a
 * reader sees, but it is not the last child of <main>: ten .bio-overlay
 * elements sit after it. They are position: fixed and carry zero flow height,
 * so they are invisible to layout and invisible to a reader, and entirely
 * visible to :last-child. The band closed 35px under its last line instead of
 * --space-3xl, which read as a page that had been cut off.
 *
 * The selector says "the last band that is not followed by anything except
 * overlays", which is the real condition. Keyed on .bio-overlay specifically
 * rather than on position: fixed, because CSS cannot select on a computed
 * value and a generic rule here would catch the next fixed component someone
 * adds for an unrelated reason. */
main > [data-surface]:not(:has(~ :not(.bio-overlay))) {
  padding-block-end: var(--space-3xl);
}

/* ---- the launcher block is clickable end to end -------------------------
 * Chip, 2026-08-04: "make the homepage blocks for the different sections
 * fully clickable rather than just the titles and the CTAs."
 *
 * THE STRETCHED LINK, not a wrapping <a>. The obvious move is to wrap the
 * whole <li> in an anchor, and it is wrong twice: it nests a link inside a
 * link (the CTA), which is invalid and unpredictable, and it hands a screen
 * reader one enormous link whose accessible name is the eyebrow, the title,
 * the description and the CTA run together. The pseudo-element covers the box
 * instead, so the DOM keeps exactly two real links with their own short names
 * and only the HIT AREA changes.
 *
 * THE CTA STAYS INDEPENDENTLY CLICKABLE, which is the part that is easy to
 * get wrong. It points somewhere more specific than the title (the Conference
 * block goes to /congress/, its CTA to /congress/#agenda), so it cannot be
 * allowed to fall under the stretched layer. Raising it into its own stacking
 * context puts it back on top: click anywhere on the block for the section,
 * click the CTA for the specific place inside it.
 *
 * TEXT IS NOT SELECTABLE INSIDE THE BLOCK, and that is the accepted cost of
 * this pattern rather than an oversight. An earlier version of this comment
 * claimed the opposite on the theory that z-index 0 leaves the overlay below
 * the type; that is wrong and was measured wrong. A positioned element paints
 * above non-positioned in-flow content in the same stacking context whatever
 * its z-index, so the overlay is over the description and takes the drag.
 * The two ways out are both worse. Giving the copy position: relative puts it
 * above the overlay and the block stops being clickable where the words are,
 * which is most of it and defeats the request. z-index -1 drops the overlay
 * behind the block's own accent background, so it stops being hittable at all.
 * Selection loses; whole-block clicking is what was asked for. */
.launcher { position: relative; }
.launcher-title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
}
/* Unconditional, not :hover scoped. A cursor only ever renders while the
 * pointer is over the element, so gating it on :hover said nothing and just
 * made the rule look conditional to the next reader. */
.launcher { cursor: pointer; }

/* ---- the hover: the block inverts, slowly ----
 * Chip, 2026-08-04: "make it kind of a slow, gradual transition... the
 * background can transition from blue to white and the font can change colour
 * as well? Instead of something underlining."
 *
 * THE UNDERLINE IS GONE. It was signalling on one word what the whole block
 * now signals, and once the entire box is a hit target, underlining the title
 * actively misleads: it points at the one element the reader does NOT have to
 * aim for.
 *
 * 420ms, NOT --t. The token is 120ms, which is the right speed for a control
 * acknowledging a click and far too fast for a field this size changing
 * colour: at 120ms a block this big does not read as transitioning, it reads
 * as flickering. Slow enough to see, short enough that the block is fully
 * white before a deliberate pointer has crossed it. ease-out so it arrives
 * gently rather than stopping dead.
 * Declared here rather than as a new global token because nothing else on the
 * site moves at this speed yet. If a second component wants it, promote it.
 *
 * EVERY INK IS RESTATED, because all four children inherit --accent-ink and
 * white on white is invisible. Measured on paper: title and description
 * --ink-900 at 17.93:1, eyebrow and CTA --accent-text at 6.46:1, which clears
 * 4.5:1 at the eyebrow's 11px. The resting state is unchanged and still
 * measures 4.59:1.
 *
 * WHEN THE THREE MARKS ARRIVE, this needs one more line. A reversed white
 * lockup is correct on the blue and invisible on the white, so img.launcher-logo
 * will need a filter or a second asset swapped at :hover. There is no mark in
 * the markup yet, so nothing is written for it. */
.launcher,
.launcher-name,
.launcher-desc,
.launcher-title a,
.launcher-cta a {
  transition: background-color 420ms ease-out, color 420ms ease-out;
}
.launcher:hover { background: var(--paper); }
.launcher:hover .launcher-title a,
.launcher:hover .launcher-desc  { color: var(--ink-900); }
.launcher:hover .launcher-name,
.launcher:hover .launcher-cta a { color: var(--accent-text); }
/* The title keeps its no-underline resting state through the hover: the
 * colour change is the whole signal now. */
.launcher:hover .launcher-title a { text-decoration: none; }

/* Above the stretched layer, and position is required for z-index to apply. */
.launcher-cta { position: relative; z-index: 1; }

/* The focus ring belongs on the title link itself, which is what is actually
 * focused. Without this the ring would be drawn around the stretched
 * pseudo-element, i.e. the entire block, which reads as a bug rather than as
 * focus and swamps the CTA's own ring sitting inside it. */
.launcher-title a:focus-visible { outline-offset: var(--rule-med); }

/* ---- the hotel row inside an accommodations disclosure -------------------
 * /congress/ #accommodations only. Photograph left, detail right.
 *
 * ONE RATIO FOR FOUR SHAPES. The four source photographs are 598, 884, 800 and
 * 1526 units tall at 1200 wide, because they are hotel marketing images
 * gathered from four places. Shown at their own ratios the Fairmont is nearly
 * three times the height of the Residence Inn and the accordion lurches every
 * time a different row opens. object-fit crops rather than squashes, and
 * centre is the safe focal point for a building shot. */
.hotel {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 6fr);
  gap: var(--space-l);
  align-items: start;
}
@media (max-width: 52rem) {
  .hotel { grid-template-columns: 1fr; gap: var(--space-m); }
}
.hotel-shot {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  object-position: center;
}
.hotel-detail > p { max-width: var(--measure); }
.hotel-detail ol { margin-block-start: var(--space-2xs); padding-inline-start: var(--space-m); }
.hotel-detail ol li { max-width: none; font-size: var(--step--1); padding-block: var(--space-3xs); }
/* Same treatment the storefront handoffs use: say where the click goes, at
 * the click, rather than leaving a reader to guess from the button. */
.hotel-away {
  font-size: var(--step--2);
  color: var(--fg-lo);
  margin-block-start: var(--space-2xs);
  max-width: none;
}

/* ---- the two billed speakers, above the grid ----------------------------
 * /congress/ #speakers. Chip, 2026-08-04: more emphasis on Kim and Tracy,
 * "like on their website".
 *
 * TWO UP AGAINST THE GRID'S FOUR, which is the whole mechanism: the cards
 * reuse .speaker-card unchanged and simply sit in a wider container, so the
 * photographs come out roughly twice the size with no second card component
 * to keep in sync. Change .speaker-card and both rows move together.
 *
 * The billing eyebrow is the only added element and it is a plain .label, so
 * it takes the site's blue like every other eyebrow. */
.speaker-feature {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-l);
  margin-block-end: var(--space-2xl);
}
@media (max-width: 52rem) {
  .speaker-feature { grid-template-columns: 1fr; gap: var(--space-xl); }
}
.speaker-card-billing { margin-block-end: var(--space-2xs); }
/* The name steps up with the photograph. The grid's name stays where it is:
 * this is the one distinction between a billed speaker and a listed one, and
 * it has to be visible at a glance or the split does nothing. */
.speaker-card--feature .speaker-card-name { font-size: var(--step-2); letter-spacing: var(--track-2); }

/* ---- the two acting rows in the Conference panel ------------------------
 * Chip, 2026-08-04: the most emphasis on the right belongs on Register and
 * Sponsor. They already lead the column; this is the weight step on top.
 *
 * A MODIFIER ON THE ROW, not a separate list. shell.js sets .nav-mega-cta
 * from an i.cta flag, so the rows remain one component and a change to
 * .link-index still reaches these two.
 * Deliberately NOT a button. A panel of text rows with two filled buttons in
 * it reads as an advert inside the navigation; the point is that these two go
 * somewhere you can act, not that they shout. Weight and ink carry it. */
.nav-mega-cta > a {
  font-weight: 600;
  color: var(--fg-hi);
}
.nav-mega-cta > a:hover { color: var(--link); }


/* ================= 46. THE VIDEO FACADE ================= */
/* ONE video on the site, on /congress/xplore/, and this component
 * exists because of a rule rather than because of a design idea.
 *
 * THE RULE: no page on this site makes an external request on load. A YouTube
 * iframe in the markup breaks that on every visit, for every reader, whether
 * or not anybody ever presses play, and it sets tracking state before a reader
 * has asked for anything. So the embed is not in the markup.
 *
 * WHAT IS IN THE MARKUP IS A LINK. .vid is an <a> to the video's own watch
 * page with a LOCALLY SERVED still inside it, so with JavaScript off a reader
 * gets a working route to the video instead of a dead poster. video.js
 * upgrades that same link into a control that swaps the embed in on click, and
 * the click is the only moment anything external is fetched. Built this way
 * round on purpose: a <div> upgraded into a link is a dead end without script,
 * and this component cannot have one.
 *
 * GRID STACKING, NOT position: absolute. The design constraints cap this site
 * at no absolute positioning for layout, so the still and the control share
 * one grid cell rather than one being pulled out of flow over the other.
 *
 * NO TRANSITION ANYWHERE HERE, so there is nothing for prefers-reduced-motion
 * to reduce. Hover moves ink, not geometry. Motion budget is 1 and a poster
 * that grows under the cursor is exactly what that dial exists to refuse. */
.vid {
  display: grid;
  border: var(--rule-hair) solid var(--rule-ink-load);
  background: var(--block);
  text-decoration: none;
}
.vid > * { grid-area: 1 / 1; }
.vid img { display: block; width: 100%; height: auto; }

/* THE SECOND SOLID ACCENT FILL ON ITS PAGE, spent knowingly. Section 8 allows
 * one, and /congress/xplore/ closes on a Request access link rather
 * than a filled button, so this is the page's only fill in practice.
 * IT HAS TO BE A FILL. Every other affordance on this site can rely on the
 * page ground behind it; this one sits on a photograph whose tones are not
 * ours and are not knowable, so nothing keyed to --fg or --rule-ink can be
 * guaranteed to clear 3:1 against it. A filled block carries its own ground
 * with it. White on the accent is 4.59:1 and is the measured pair section 8
 * already ships. */
.vid-play {
  place-self: center;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-m);
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--step--1);
}
.vid:hover .vid-play { background: var(--accent-text); }
/* The triangle, drawn rather than fetched: an icon file would be a second
 * request for eleven pixels of geometry. currentColor, so it can never drift
 * from the label beside it. */
.vid-tri {
  width: 0.55em;
  height: 0.7em;
  background: currentColor;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* The player, once a reader has asked for it. 16 by 9 is declared here and on
 * the still above, so the swap costs no reflow and nothing below it moves. */
.vid-frame {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: var(--rule-hair) solid var(--rule-ink-load);
}

/* ---- a two-up storefront row -------------------------------------------
 * .store is authored for three and steps 3 / 2 / 1. /congress/partnerships/
 * carries exactly two, which left a third of the row empty at desktop. Two
 * columns straight to one, and the existing 44rem collapse point is reused so
 * the pair breaks where every other pair on the site breaks. */
.store--pair { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 44rem) {
  .store--pair { grid-template-columns: 1fr; }
}

/* ---- a form runs the full column ---------------------------------------
 * Built for /requirement/. Chip, 2026-08-04: "The form should also be full width."
 * NOW THE SITE'S FORM LAYOUT, used on /requirement/, /congress/register/ and
 * /congress/sponsor/ as of 2026-08-04. The class name is kept rather than
 * renamed: three pages reference it, and a rename buys nothing a comment
 * cannot say. Any page whose main content is a form takes .req-form.
 *
 * The page is one long form, so the .editorial reading rail that serves prose
 * works against it here: a textarea held to --measure while its label sits in
 * a 12-column grid leaves the right half of the page empty on every row, and
 * the questions stop reading as one sequence.
 * The two long-answer fields get the whole column; the short ones stay short,
 * because a name input the width of the page is a worse target, not a better
 * one, and it lies about how much is wanted. */
.req-form .editorial > * { grid-column: main; }
.req-form fieldset { max-width: none; }
.req-form textarea { inline-size: 100%; max-width: none; }
.req-form input[type="text"],
.req-form input[type="email"],
.req-form input[type="date"],
.req-form select { inline-size: 100%; max-width: 34rem; }
.req-form .field-hint { max-width: var(--measure); }

/* ================= 47. THE READINESS SCALES ================= */
/* /xpand/#what-readiness-means only. Two nine-point tracks, TRL and CRL,
 * shown together because ConvergX runs both in prose on its own site and
 * never puts them side by side. The whole idea is that the same nine
 * numbers answer two different questions.
 *
 * NO NEW TOKENS, NO NEW COLOURS. In-scope cells take --accent with
 * --accent-ink on top, which is the same solid the buttons use; out-of-scope
 * cells are the page's ordinary block-on-surface treatment. So the figure
 * inverts correctly on all three surfaces without a single hand-written
 * value, and it stays correct if the palette is ever replaced with Lindsay's
 * real one.
 *
 * NINE EQUAL COLUMNS, and they collapse to a wrap rather than a scroll. A
 * horizontal scroller hides half a nine-point scale on a phone, which
 * defeats the only thing the figure is for. At narrow widths the row wraps
 * to two lines and stays fully visible. */
.rl {
  margin-block-start: var(--space-xl);
  display: grid;
  gap: var(--space-xl);
}
.rl-name { margin-block-end: var(--space-3xs); }
.rl-q {
  font-family: var(--font-display);
  font-size: var(--step-1);
  line-height: var(--lh-heading);
  color: var(--fg-hi);
  margin-block-end: var(--space-s);
  max-width: none;
}
.rl-steps {
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  gap: var(--rule-gap);
  list-style: none;
  margin: 0;
  padding: 0;
}
.rl-steps > li {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  text-align: center;
  padding-block: var(--space-xs);
  background: var(--block);
  color: var(--fg-lo);
  border: var(--rule-hair) solid var(--rule-ink-load);
  border-radius: var(--radius);
}
/* The stated scope. Solid, so the band reads at a glance from across the
 * page, which is the one thing a reader is meant to take from the figure. */
.rl-steps > .rl-in {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  font-weight: 600;
}
.rl-note {
  font-size: var(--step--1);
  color: var(--fg-lo);
  margin-block-start: var(--space-xs);
  max-width: var(--measure);
}
.rl figcaption { margin-block-start: var(--space-s); }

/* Two tracks side by side once there is room for eighteen cells without
 * either row of numbers crowding. Below it they stack, which reads as two
 * scales one after the other and is the correct fallback: the comparison
 * survives stacking, the numbers do not survive squeezing. */
@media (min-width: 56rem) {
  .rl { grid-template-columns: 1fr 1fr; gap: var(--space-2xl); }
  .rl figcaption { grid-column: 1 / -1; }
}

/* ================================================================
   SECTION 48: SPONSOR REEL (2026-08-12). One slow, continuous strip
   of the Congress sponsor marks on the homepage, under the congress
   band. The track holds TWO identical .sponsor-set lists and slides
   half its own width, so the loop is seamless; each set carries half
   the inter-set gap as inline padding to keep the junction rhythm.
   Marks are the same masked, cap-height-normalised component the
   proofbar uses, one size up. Reduced motion gets a static wrapped
   row and the duplicate set display:none. No JS.
   ================================================================ */
.sponsor-reel { overflow: hidden; margin-block-start: var(--space-l); }
.sponsor-reel-track {
  display: flex;
  width: max-content;
  animation: sponsor-reel 80s linear infinite;
}
.sponsor-set {
  display: flex;
  align-items: center;
  gap: var(--space-3xl);
  list-style: none;
  margin: 0;
  padding: 0;
  padding-inline: calc(var(--space-3xl) / 2);
}
.sponsor-reel .mark { --mark-cap: 1.35rem; } /* 1rem -> 1.35 (Chip 2026-08-14: bigger) */
@keyframes sponsor-reel { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  .sponsor-reel-track { animation: none; width: auto; flex-wrap: wrap; }
  .sponsor-set[aria-hidden="true"] { display: none; }
  .sponsor-set { flex-wrap: wrap; row-gap: var(--space-l); }
}

/* ================================================================
   SECTION 49: STEEL CONGRESS BAND (2026-08-12, Chip). The homepage
   congress band takes the notice bar's chrome fill. Chrome is a
   MID-TONE: white fails 4.5:1 on it (3.43:1) and navy fails even the
   3:1 boundary floor (2.38:1), so the band re-points the semantic
   inks locally: type is ink-900 (5.23:1 on chrome, AA), the eyebrow
   likewise, and the button borrows the notice bar's own measured
   pairing (paper fill, 3.43:1 boundary; navy text on paper, 13.57:1).
   ================================================================ */
.congress-band--steel {
  background: var(--chrome);
  --fg: var(--ink-900);
  --fg-hi: var(--ink-900);
  --label-ink: var(--ink-900);
  color: var(--ink-900);
  padding-block: var(--space-2xl);   /* a filled band cannot sit on the
                                        section's default open bottom */
}
.congress-band--steel .btn--solid {
  background: var(--paper);
  border-color: var(--paper);
  color: var(--accent-text);
}

/* ================================================================
   SECTION 50: CONGRESS DOORS SPLIT (2026-08-12, Chip). Prose left,
   the four congress links alone on the right. A plain grid on
   purpose: .editorial's named-line system auto-places lede and body
   onto shared rows and cannot hold this shape without leaving holes.
   The section also closes the light band, so it carries its own
   bottom interval before the ground change.
   ================================================================ */
#congress-doors { padding-block-end: var(--space-4xl); }
.congress-split { display: grid; gap: var(--space-2xl); align-items: start; }
@media (min-width: 64rem) {
  .congress-split { grid-template-columns: 7fr 5fr; gap: var(--space-3xl); }
}
.congress-split-copy > p + p { margin-block-start: var(--space-m); }
.congress-split-copy .lede-text { margin-block-end: var(--space-l); }

/* SECTION 51 RETIRED 2026-08-13: the in-page wall now adopts the footer
   quotes grammar wholesale (.footer-quotes + .footer-quote-grid on the
   section, tokens invert for the light ground automatically). Full quotes
   render because the page cards carry the complete published text and
   nothing clamps them; min-block-size still applies, cards grow. */

/* SECTION 52 (2026-08-13): two-column mega list. First use: the Consulting
   panel, six rows that left half the panel empty in one column. Row-flow
   grid, so reading order stays left-right; the first TWO items sit on the
   top row and both drop the border the single-column rule only stripped
   from the first. */
.mega-col--two .link-index {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--space-2xl);
}
.mega-col--two .link-index li:nth-child(2) { border-top: 0; }

/* SECTION 53 (2026-08-13, Lindsay working session): clean mega menus.
   The item blurbs read as AI filler and are off for launch; link type
   steps up one size to carry the panel alone. The blurb markup stays in
   shell.js so restoring them is deleting two lines here. */
.mega .descriptor { display: none; }
.mega .link-index a { font-size: var(--step-1); }

/* Whitespace below the sponsor wall (Chip, twice): the logo rows sat flush
   against the ground change. The first fix used margin on the wall, which
   COLLAPSES through the section boundary and vanishes into the next band.
   Padding on the section cannot collapse. */
#sponsors .proofbar { margin-block-start: var(--space-l); }
#sponsors.section--dense { padding-block-end: var(--space-3xl); }

/* Edge fades on the sponsor reel (Chip, 2026-08-13): the strip runs wider
   than the container and dissolves before the viewport edges. A MASK, not a
   painted gradient, so it matches whatever ground the reel sits on without
   naming a colour. */
.sponsor-reel {
  /* Fade begins AT the container edge (Chip): marks are fully opaque across
     the page column and dissolve only in the margin between the container
     and the viewport. On viewports narrower than the container the gutter
     keeps a slim fade. */
  --reel-edge: max(var(--gutter), calc((100% - var(--page-max)) / 2));
  -webkit-mask-image: linear-gradient(to right,
    transparent 0, #000 var(--reel-edge),
    #000 calc(100% - var(--reel-edge)), transparent 100%);
          mask-image: linear-gradient(to right,
    transparent 0, #000 var(--reel-edge),
    #000 calc(100% - var(--reel-edge)), transparent 100%);
}

/* SECTION 55 (2026-08-13): collapsed team bios. Names and roles show; the
   prose sits behind the interim site's own affordance, "View full bio",
   as a native disclosure. No JS. */
.bio-more > summary {
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  margin-block-start: var(--space-s);
  width: fit-content;
}
.bio-more > summary::-webkit-details-marker { display: none; }
.bio-more { margin-block-start: var(--space-2xs); }
.bio-more[open] > summary { margin-block-end: var(--space-s); }

/* ========================================================================
 * SECTION 56 (2026-08-13, Chip): the who-attends industries as CARDS.
 * Chip: "Can you make these cards instead on the Congresses page?" The
 * eight industries were .whatis rows (title left, body right); they are
 * now a two-up card grid. The card ground is --block, the token whose
 * stated job is "a panel on a white page", so the cards read against the
 * paper ground without needing a border. The h3 keeps the .whatis-title
 * ramp so the page's subheading scale stays one scale.
 * ==================================================================== */
.industry-cards {
  display: grid;
  gap: var(--space-m);
  margin-block-start: var(--space-xl);
}
@media (min-width: 48rem) {
  .industry-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.industry-card {
  /* ICE, not the warm grey (Chip 2026-08-14: "they don't use this grey
   * anywhere"). Ice #C4D2E2 is the brand's light tone; ink-900 headings
   * (~13:1) and cool-700 body (~9:1) both clear AA on it comfortably. */
  background: var(--ice);
  padding: var(--space-l);
}
.industry-card > h3 {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: var(--track-2);
  line-height: var(--lh-heading);
  color: var(--fg-hi);
  text-wrap: balance;
  margin-block-end: var(--space-s);
}
.industry-card > p { max-width: none; }

/* ========================================================================
 * SECTION 57 (2026-08-13, Chip): the commercialization cluster (/xpand/
 * #the-last-mile + #what-readiness-means) on the BRAND NAVY so it pops
 * from the black ground. Navy fails AS an accent on dark (1.32:1 against
 * the ground) but works AS a ground: white type on navy is 13.57:1 and
 * the ice accent holds ~8.7:1. --bg follows so anything painting
 * var(--bg) inside the band matches.
 * ==================================================================== */
.band--navy {
  background: var(--navy);
  --bg: var(--navy);
}

/* ========================================================================
 * SECTION 58 (2026-08-13, Chip): team layout. Kim and Jessilyn hold full
 * .bio-figure rows with real air between them; the remaining eight sit in
 * a three-wide grid, photo on top, name and title under it, bios still
 * collapsed behind View full bio. Below 48rem the grid folds to one
 * column and everyone reads as a single stack again.
 * ==================================================================== */
.team-list > .bio-figure + .bio-figure,
.team-list > * + .team-grid { margin-block-start: var(--space-3xl); }
.team-grid {
  display: grid;
  gap: var(--space-2xl) var(--space-l);
}
@media (min-width: 48rem) {
  .team-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Inside the grid the two-column figure folds: one track, photo first. */
.team-grid > .bio-figure {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-m);
}
.team-grid .bio-photo { max-inline-size: none; }

/* Adam's source is a wide conference shot, not a studio square; the 1/1
 * cover crop was taking the top of his head. Bias the crop upward. */
#card-adam-frost .bio-photo,
#bio-adam-frost .bio-photo { object-position: 50% 10%; }

/* SECTION 59 (2026-08-13, Chip: "Make these logos larger"): the sponsor
 * marks on /congress/#sponsors step up from the site-wide 0.75rem cap to
 * 1.1rem. Scoped to that section so the hero reel and the other proofbars
 * keep their own size. */
#sponsors .mark { --mark-cap: 1.45rem; } /* 1.1 -> 1.45 (Chip 2026-08-14: bigger) */

/* SECTION 58b (2026-08-13, Chip): Kim's and Jessilyn's photos take half the
 * container, names to the right. Only the two full rows; the grid keeps its
 * own single-track figure. The 18rem photo cap is overridden here on
 * purpose: both source files are the 800px studio squares and hold up at
 * half the page width. */
.team-list > .bio-figure {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

/* SECTION 60 (2026-08-13, Chip): the app screenshot is a full phone-height
 * capture and ran far past the copy beside it. The figure now takes exactly
 * the copy column's height (height:0 + min-height:100% inside the stretched
 * grid row, so the COPY sets the row) and the capture fades out over its
 * last stretch instead of ending on a hard cut. Below the stack breakpoint
 * the trick would measure an auto row, so a plain max-height takes over. */
@media (min-width: 60rem) {
  .app-band .app-inner { align-items: stretch; }
  .app-band .mod-shot-frame {
    height: 0;
    min-height: 100%;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, #000 72%, transparent 98%);
            mask-image: linear-gradient(to bottom, #000 72%, transparent 98%);
  }
}
@media (max-width: 59.99rem) {
  .app-band .mod-shot-frame {
    max-height: 30rem;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, #000 72%, transparent 98%);
            mask-image: linear-gradient(to bottom, #000 72%, transparent 98%);
  }
}

/* SECTION 61 (2026-08-13, Chip): the About hero split. Heading left, the
 * founding-observation lede in the right half, aligned to the heading's
 * baseline area, so the hero reads balanced. (The team composite that
 * briefly lived here was cut the same day.) */
.about-hero-split {
  display: grid;
  gap: var(--space-l) var(--space-xl);
  align-items: end;
}
@media (min-width: 64rem) {
  .about-hero-split { grid-template-columns: minmax(0, 6fr) minmax(0, 6fr); }
}

/* SECTION 62 (2026-08-14, Chip): the sponsorship contact section reshaped.
 * Intro (eyebrow, h2, description) holds the left half, top-aligned with
 * the form; the form sits in a --block panel with more air between fields.
 * One column below 60rem, intro above form. */
.sponsor-split {
  display: grid;
  gap: var(--space-xl);
  align-items: start;
}
@media (min-width: 60rem) {
  .sponsor-split { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.sponsor-intro > .label { display: block; margin-block-end: var(--space-s); }
.sponsor-intro > h2 { margin-block-end: var(--space-m); }
.sponsor-intro > p { max-width: var(--measure); }
.sponsor-form-box {
  background: var(--block);
  padding: var(--space-xl);
}
.sponsor-form-box .field + .field { margin-block-start: var(--space-l); }
.sponsor-form-box .field > label { display: block; margin-block-end: var(--space-2xs); }
.sponsor-form-box > p:last-child { margin-block-start: var(--space-l); }

/* SECTION 60b (2026-08-14, Chip): the app capture was "too cut off" at full
 * column width; the height cap showed only the status bar and header.
 * Narrower frame, same cap: more of the actual app fits the slice. */
.app-band .mod-shot-frame {
  max-inline-size: 20rem;
  margin-inline: auto;
}

/* SECTION 63 (2026-08-14, Chip): /xpand/#our-process ran straight into the
 * navy band below it with no air. The section was promoted out of
 * #what-xpand-does, whose inner sections carried their own end padding;
 * top-level bare sections do not. */
#our-process { padding-block-end: var(--space-3xl); }

/* SECTION 60c (2026-08-14, Chip): eyebrow and h2 live inside the app copy
 * column now, so the capture top-aligns with the title and the taller
 * column height shows more of the app through the same fade cap. */
.app-copy > .label { display: block; margin-block-end: var(--space-s); }
.app-copy > h2 { margin-block-end: var(--space-l); }

/* SECTION 64 (2026-08-14, Chip live review): the boxed sponsorship form ran
 * to the section's bottom edge; give the section real end padding. */
#sponsor-contact { padding-block-end: var(--space-3xl); }

/* ================= 60. THE NOTICE BAR AT PHONE WIDTH ==================
 * ADDED 2026-08-14 during the WordPress port. This is a fix to the design
 * system, not a WordPress workaround, and it should go back to the static
 * tree with it.
 *
 * THE DEFECT. Section 45's ladder is explicit that "the date is never
 * dropped": it was the last resort and the measurement never reached it.
 * That measurement was taken when the countdown was a CLOCK. Section 57
 * then rebuilt the countdown as PROSE ("Only 38 days, 9 hours, 48 minutes
 * and 43 seconds away.") and undid the tier-2 and tier-4 clips, on the
 * reasoning that the narrow-end budget is met by the type step alone.
 *
 * It is not. Measured at 375px: the countdown takes the row to where
 * .notice-text is handed 92px for 319px of content. With the nowrap plus
 * overflow:hidden from section 45 still in force, the date does not
 * degrade, it TRUNCATES MID-WORD, and the bar on every page of the site
 * reads "Celebrate 10 ye". That is the date being dropped, which is the
 * one thing the ladder says must not happen.
 *
 * THE FIX, and why it is this one. The row gives up its single-line rule
 * at phone width instead of giving up the date. That was the original
 * behaviour the pre-2026-07-30 note describes ("at 375px it wraps to two
 * lines"), and it is the cheapest thing left on the ladder now that the
 * clip tiers are gone.
 *
 * WHAT IS NOT SPENT, unchanged from section 45: the CTA keeps flex: none
 * and is never squeezed, because a Register button reading "Regis..." is
 * worse than a second line. Nothing here is removed from the
 * accessibility tree. No rule counts children, so the two-child (post
 * Congress, countdown removed) and no-JS rows both wrap correctly.
 *
 * Scoped to 30rem so every width the ladder already measured at 375, 390
 * and 414 is inside it, and nothing at tablet or above changes. */
@media (max-width: 30rem) {
  .notice-inner {
    flex-wrap: wrap;
    row-gap: var(--space-3xs);
  }

  /* Undo section 45's one-line clamp. The date is the thing being
   * protected, so it is the thing allowed to wrap. */
  .notice-text {
    white-space: normal;
    overflow: visible;
    flex-basis: 100%;
  }
}

/* ============== 61. TOUCH TARGETS =====================================
 * ADDED 2026-08-16 during the WordPress port. A design-system fix, not a
 * WordPress one: it belongs back in the static tree too.
 *
 * MEASURED, NOT GUESSED. At 375px, twelve controls that are their own box
 * (not inline links in running text, which have their own exception) render
 * under the 44px floor. The two that matter most are the ones a phone reader
 * touches first and most often:
 *
 *   .nav-mobile > summary   34px   the ONLY way to open the nav on a phone
 *   .notice-cta             38px   Register, the site's primary CTA, on
 *                                  every page
 *   .logo--mark             24px   the link home
 *   .sess > summary         21-41  the agenda's expandable sessions
 *   .skip                   37px   the keyboard skip link
 *
 * GATED ON pointer: coarse, NOT on width. The floor exists because a
 * fingertip is about 9mm and a mouse cursor is one pixel, so the condition
 * is the input device rather than the viewport. This way a narrow desktop
 * window keeps its compact controls and a large tablet still gets the
 * bigger targets.
 *
 * NOTHING MOVES VISUALLY. Every rule below grows the HIT AREA using
 * min-height plus centring, so the type and the marks stay exactly where
 * the design puts them. No font size, no margin and no colour changes here.
 */
@media (pointer: coarse) {

  /* The mobile nav toggle. Widened as well as heightened: at 68px it was
   * both short and narrow, in the corner of the screen, which is the
   * hardest place on a phone to hit accurately. */
  .nav-mobile > summary {
    min-height: 44px;
    min-width: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* Register. The one action the notice bar exists to get. */
  .notice-inner a.notice-cta {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* The logo is a link home. The mark keeps its size; the box around it
   * grows to meet the floor. */
  .logo--mark {
    min-height: 44px;
    display: flex;
    align-items: center;
  }

  /* Agenda sessions and the day disclosures. A row the reader is invited to
   * open has to be openable with a thumb. */
  .mod-faq-list summary,
  .faq > summary,
  .sess > summary {
    min-height: 44px;
    display: flex;
    align-items: center;
  }

  /* Buttons and form controls across the site, including WooCommerce's own.
   * min-height rather than height so a two-line label still grows. */
  .btn,
  .cx-form button,
  .cx-form input,
  .cx-form select,
  .cx-form textarea {
    min-height: 44px;
  }

  /* The skip link only appears on focus, but when it appears it is the
   * control a keyboard or switch user is aiming at. */
  .skip { min-height: 44px; display: flex; align-items: center; }
}

/* ================= 62. THE XPAND TRL TABLE ================= */
/* Lindsay's NASA-vs-Xpand comparison (email 2026-08-17), rebuilt as a
 * native table rather than her screenshot: the attachment sat on a stock
 * photograph and carried its own typography, and a table is content, not
 * an image. It renders on the navy band, so every colour is a token the
 * band already resolves. */

.trl-table-wrap { overflow-x: auto; margin-block-start: var(--space-xl); }

.trl-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
  text-align: start;
}

.trl-table caption {
  text-align: start;
  font-weight: 600;
  color: var(--fg-hi);
  margin-block-end: var(--space-s);
}

.trl-table th,
.trl-table td {
  padding: var(--space-s) var(--space-m) var(--space-s) 0;
  border-block-start: var(--rule-hair) solid var(--rule-ink);
  vertical-align: top;
  text-align: start;
}

.trl-table thead th {
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--fg-lo);
  border-block-start: 0;
}

.trl-table tbody th[scope="row"] {
  font-weight: 600;
  color: var(--fg-hi);
  white-space: nowrap;
}

/* ================= 63. FORM HONEYPOT ================= */
/* Hidden from people, left in the DOM for naive bots. Off-canvas rather
 * than display:none so it still looks like a field to scrapers. */
.cx-hp {
  position: absolute !important;
  left: -10000px !important;
  top: auto !important;
  width: 1px !important;
  height: 1px !important;
  margin: -1px !important;
  padding: 0 !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  clip-path: inset(50%) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

.cx-hp input {
  min-height: 0 !important;
}
