/* ==================== layer: hexa-ops ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS — page styles.
   ----------------------------------------------------------------------------
   Loaded after the shared precision stylesheets. It declares three things and
   nothing else:

     1  RESTATED KIT RULES. This line does not load `base.css` or
        `components.css`, so `ComparisonTable`, `MetricsTable`, `SplitPathCTA`
        and `Placeholder` arrive with correct markup and no styling at all.
        That has already shipped twice on this project (CHOICES.md §13), so the
        rules are restated here against this page's tokens rather than the
        stylesheets being pulled in wholesale.

     2  THE THREE GRAPHICS. Production cycle, buffer waterfall, four-bar waste
        chart.

     3  THE BANDS this asset has and asset 01 does not.

   No colour is hardcoded. The accent family comes from the theme block in
   `themes.css`; everything else is the shared neutral scale.

   THE SPACING SCALE STEPS 4, 8, 12, 16, 24, 32, 48, 64, 96, 128, 160, 200 and
   HAS NO `--s-5`, `--s-10` OR `--s-20`. An undefined custom property does not
   warn, does not error and does not show as broken in a styles panel: the
   declaration is dropped and the property keeps its initial value.
   `tools/check-tokens.mjs` fails the build on one.
   ========================================================================= */

/* ============================================================================
   1 · RESTATED KIT RULES
   ----------------------------------------------------------------------------
   The shared components use the kit's class names (`reveal`, `mono`,
   `visually-hidden`), which live in stylesheets this page does not load.
   ========================================================================= */

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

.mono {
  font-family: var(--font-mono);
  font-size: var(--text-label);
  font-weight: var(--weight-medium);
  line-height: var(--leading-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-tertiary);
}

/* The kit's reveal wrapper, matched to `.p-reveal` so a shared component
   enters at the same speed and on the same curve as everything around it. */
.js .reveal {
  opacity: 0;
  transform: translate3d(0, var(--travel), 0);
  transition: opacity var(--slow) var(--ease-out),
    transform var(--slow) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

.js .reveal.is-revealed {
  opacity: 1;
  transform: none;
  will-change: auto;
}

@media (prefers-reduced-motion: reduce) {
  .js .reveal { opacity: 1; transform: none; }
}

/* --- Placeholder · the visible-gap component ------------------------------
   An unfilled slot is never silently omitted and never quietly filled. It is
   dashed, labelled, and says where the missing thing would come from. */
.placeholder {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-6);
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--radius-md);
  background: var(--accent-tint);
  text-align: left;
  max-width: var(--measure);
  margin-inline: auto;
}

.placeholder__label {
  font-family: var(--font-mono);
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--accent);
}

.placeholder__what { font-size: var(--text-small); color: var(--ink-secondary); }

.placeholder__source {
  font-size: var(--text-small);
  color: var(--ink-tertiary);
}

/* --- ComparisonTable ------------------------------------------------------
   Four columns on desktop, an accordion below the breakpoint. Never a
   horizontal scroll: a four-column table on a 375px screen is a table nobody
   reads.

   Each row is a `<details open>`. Above the breakpoint the disclosure marker
   is hidden and the row lays out as a grid, so the element that collapses on
   a phone is the same element that reads as a table row on a laptop. With
   JavaScript off every row stays open, because `open` is in the markup. */
.comparison {
  border-top: 1px solid var(--hairline-strong);
}

.comparison__head {
  display: none;
  gap: var(--s-6);
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--hairline);
}

.comparison__row {
  border-bottom: 1px solid var(--hairline);
  padding-block: var(--s-6);
}

.comparison__summary {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
  cursor: pointer;
  list-style: none;
  color: var(--ink);
  font-size: var(--text-body);
  font-weight: var(--weight-medium);
  line-height: 1.4;
}

.comparison__summary::-webkit-details-marker { display: none; }

.comparison__summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

/* The marker is a plus that becomes a minus. Drawn from two rules rather than
   a glyph, so it cannot pick up a font's own idea of vertical centring. */
.comparison__marker {
  position: relative;
  flex: none;
  width: 14px;
  height: 14px;
  margin-top: 4px;
}

.comparison__marker::before,
.comparison__marker::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: var(--ink-tertiary);
  transition: transform var(--fast) var(--ease-out);
}

.comparison__marker::after { transform: rotate(90deg); }

.comparison__row[open] .comparison__marker::after { transform: rotate(0deg); }

.comparison__cell {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding-top: var(--s-4);
  min-width: 0;
}

.comparison__cell-label { color: var(--ink-tertiary); }

.comparison__cell > span:not(.comparison__cell-label) {
  color: var(--ink-secondary);
  font-size: var(--text-small);
  line-height: 1.55;
}

.comparison__concept {
  color: var(--ink) !important;
  font-weight: var(--weight-medium);
}

.comparison__translation {
  color: var(--ink-tertiary);
  font-size: var(--text-small);
  line-height: 1.55;
}

.comparison__translation em {
  font-style: normal;
  font-family: var(--font-mono);
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  color: var(--accent);
}

@media (min-width: 64em) {
  .comparison__head {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 3fr) minmax(0, 3fr) minmax(0, 4fr);
  }

  .comparison__row {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 3fr) minmax(0, 3fr) minmax(0, 4fr);
    gap: var(--s-6);
    align-items: start;
  }

  /* WITHOUT THIS THE FOUR-COLUMN TABLE IS NOT A TABLE. Current Chrome wraps
     everything after `<summary>` in a `::details-content` pseudo-element, so a
     `<details>` laid out as a grid gets TWO items: the summary, and one box
     holding all three cells. The row still reports `display: grid`, the CSS
     still reads correctly, and the three method columns silently stack inside
     column two. Measured, not spotted: every cell returned the same x.

     `display: contents` dissolves that wrapper so the cells are grid items
     again. A browser without `::details-content` never made the wrapper and
     ignores the rule, so both behave the same. */
  .comparison__row::details-content { display: contents; }

  .comparison__summary { display: block; cursor: default; }

  .comparison__marker { display: none; }

  .comparison__cell { padding-top: 0; }

  .comparison__cell-label { display: none; }
}

/* --- MetricsTable ---------------------------------------------------------
   Horizontal rules only. An unfilled row carries the placeholder visual
   language, so an empty cell can never be mistaken for a rendering fault. */
.metrics__table { width: 100%; }

.metrics__table th,
.metrics__table td {
  text-align: left;
  vertical-align: top;
  padding: var(--s-4) var(--s-4) var(--s-4) 0;
  border-bottom: 1px solid var(--hairline);
}

.metrics__table thead th {
  border-bottom: 1px solid var(--hairline-strong);
  color: var(--ink-tertiary);
  white-space: nowrap;
}

.metrics__table tbody th {
  color: var(--ink);
  font-weight: var(--weight-medium);
}

.metrics__table td {
  color: var(--ink-secondary);
  font-size: var(--text-small);
  line-height: 1.55;
}

.metrics__row--unfilled th,
.metrics__row--unfilled td { color: var(--ink-tertiary); }

.metrics__slot {
  display: inline-block;
  padding: 2px var(--s-2);
  border: 1px dashed var(--hairline-strong);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ink-secondary);
}

.metrics__note {
  margin-top: var(--s-6);
  color: var(--ink-tertiary);
  font-size: var(--text-small);
  max-width: var(--measure);
}

/* Below the breakpoint the table reflows to stacked cards. The `data-label`
   attributes carry the column names into the stacked view, so a cell never
   loses the header that gave it meaning. */
@media (max-width: 47.99em) {
  .metrics__table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

  .metrics__table tr {
    display: block;
    padding-block: var(--s-4);
    border-bottom: 1px solid var(--hairline);
  }

  .metrics__table th,
  .metrics__table td {
    display: block;
    border: 0;
    padding: 0 0 var(--s-2) 0;
  }

  .metrics__table td::before {
    content: attr(data-label);
    display: block;
    font-family: var(--font-mono);
    font-size: var(--text-label);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--ink-tertiary);
    margin-bottom: 2px;
  }
}

/* --- SplitPathCTA · three doors ------------------------------------------ */
.split-path__headline {
  font-size: var(--text-h3);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-h3);
  color: var(--ink);
  margin-bottom: var(--s-8);
}

.split-path__doors {
  display: grid;
  gap: var(--grid-gap);
  text-align: left;
}

@media (min-width: 48em) {
  .split-path__doors { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.split-path__door {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-3);
  min-width: 0;
  padding: var(--card-pad);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface);
  transition: border-color var(--fast) var(--ease-out);
}

.split-path__door:hover { border-color: var(--hairline-strong); }

.split-path__who {
  font-size: var(--text-body);
  font-weight: var(--weight-medium);
  color: var(--ink);
}

.split-path__body {
  flex: 1;
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

/* ============================================================================
   2 · SHARED BAND FURNITURE
   ========================================================================= */

.hx-label { color: var(--accent); margin-bottom: var(--eyebrow-gap); }

.hx-headline { max-width: 22ch; }

.hx-lede {
  margin-top: var(--heading-gap);
  max-width: var(--measure);
  color: var(--ink-secondary);
}

.hx-subhead { max-width: 26ch; }

/* Space between elements inside a band, always less than the space between
   bands. `--section-pad` is 72px on mobile and 128px at desktop, so 96px is
   the largest in-band step that keeps the 2× rule intact at the top end. */
.hx-band { margin-top: var(--s-16); }

@media (min-width: 64em) {
  .hx-band { margin-top: var(--s-24); }
}

/* ============================================================================
   3 · THE PRODUCTION CYCLE  (§1.4)
   ----------------------------------------------------------------------------
   A rail with three phases on it, each marked irreversible. The point of the
   graphic is the ORDER and the impossibility of going back, so the rail runs
   one way and the nodes are square rather than round: a circle on a track
   reads as a station you could return to.
   ========================================================================= */

.hx-cycleband { padding-block: var(--s-24); }

.hx-cycle__label { color: var(--accent); margin-bottom: var(--s-12); }

/* NO PADDING ON THE TRACK. The rail is absolutely positioned against this
   element, so a padding-top here moves the phases down and leaves the rail
   behind: it drew 43px above the nodes it is supposed to run through. The
   space above the track belongs to the label's margin instead. */
.hx-cycle__track {
  position: relative;
  display: grid;
  gap: var(--s-8);
}

/* The rail. Drawn behind the nodes, and it is the element that draws itself on
   entry: everything else simply arrives. */
.hx-cycle__track::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 5px;
  width: 1px;
  background: var(--hairline-strong);
  transform-origin: top;
}

.hx-cycle__phase {
  position: relative;
  padding-left: var(--s-8);
  min-width: 0;
}

.hx-cycle__node {
  position: absolute;
  left: 0;
  top: 6px;
  width: 11px;
  height: 11px;
  background: var(--canvas);
  border: 1px solid var(--accent);
}

.hx-cycle__name {
  display: block;
  font-size: var(--text-h3);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-h3);
  line-height: var(--leading-h3);
  color: var(--ink);
}

.hx-cycle__note {
  display: block;
  margin-top: var(--s-1);
  font-size: var(--text-small);
  line-height: 1.55;
  color: var(--ink-secondary);
}

.hx-cycle__tag {
  display: inline-block;
  margin-top: var(--s-3);
  padding: 2px var(--s-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-sm);
  color: var(--ink-tertiary);
}

.hx-cycle__caption {
  margin-top: var(--s-12);
  font-size: var(--text-small);
  color: var(--ink-tertiary);
}

@media (min-width: 48em) {
  .hx-cycle__track {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s-12);
  }

  .hx-cycle__track::before {
    inset: 5px 0 auto 0;
    width: auto;
    height: 1px;
  }

  .hx-cycle__phase { padding-left: 0; padding-top: var(--s-8); }

  .hx-cycle__node { top: 0; left: 0; }
}

.hx-factstrip {
  margin-top: var(--s-24);
  padding-top: var(--s-12);
  border-top: 1px solid var(--hairline-strong);
}

/* ============================================================================
   4 · THE BUFFER WATERFALL  (§5.4)
   ----------------------------------------------------------------------------
   The floor of the plot is the baseline, named on the page. Every bar is a
   floating step measured from it, and the last column is the sum of the
   others. See the component file for why this is a waterfall rather than a
   stacked column.
   ========================================================================= */

.hx-wfall__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2) var(--s-6);
  margin-bottom: var(--s-8);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--hairline);
}

.hx-wfall__label { color: var(--accent); }

/* The floor of the plot is the baseline, so the baseline is named where the
   reader learns how to read the axis rather than inside it. */
.hx-wfall__floor { color: var(--ink-tertiary); }

.hx-wfall__plot {
  position: relative;
  height: clamp(220px, 32vw, 340px);
  /* A gutter on the right for the gridline values, so a tick label never sits
     on top of the bar nearest it. */
  padding-right: 40px;
  margin-bottom: 96px;   /* room for the column names below the floor */
  border-bottom: 1px solid var(--ink-tertiary);
}

@media (min-width: 48em) {
  .hx-wfall__plot { margin-bottom: 64px; padding-right: 48px; }
}

/* Gridlines. Hairlines at low opacity, never a heavier line at a lighter
   value, so they sit correctly and survive a palette swap. */
.hx-wfall__grid { position: absolute; inset: 0; }

.hx-wfall__tick {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--hx-at);
  border-top: 1px solid var(--hairline);
}

/* The value sits BELOW its own gridline, not above it. Above, the topmost
   label's box extends past the top of the plot, so anything clipping the plot
   area cuts it off. Below, every label is inside the box it belongs to, which
   is also the conventional place for a gridline value. */
.hx-wfall__tick span {
  position: absolute;
  right: 0;
  top: 2px;
  color: var(--ink-tertiary);
}

.hx-wfall__cols {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: var(--s-2);
  height: 100%;
}

@media (min-width: 48em) {
  .hx-wfall__cols { gap: var(--s-6); }
}

/* `min-width: 0` on every level. A grid or flex item defaults to
   `min-width: auto` and refuses to shrink below its content, which is how a
   frame gets pushed past its own edge at 375px while a page-level overflow
   check passes because the frame is cropping the evidence. */
.hx-wfall__col {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
}

.hx-wfall__bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--hx-bottom);
  height: var(--hx-height);
  background: var(--accent);
  border-radius: 2px;
  transform-origin: bottom;
}

.hx-wfall__col--total .hx-wfall__bar {
  background: var(--accent-tint);
  border: 1px solid var(--accent);
}

.hx-wfall__value {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--hx-bottom) + var(--hx-height) + 6px);
  text-align: center;
  color: var(--ink);
}

.hx-wfall__name {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + var(--s-3));
  font-size: var(--text-label);
  line-height: 1.3;
  text-align: center;
  color: var(--ink-secondary);
  /* `anywhere` broke "Production" as "Producti / on" in a 55px column at
     375px. `break-word` only breaks when nothing else fits, and automatic
     hyphenation puts a hyphen where it does. */
  overflow-wrap: break-word;
  hyphens: auto;
}

.hx-wfall__col--total .hx-wfall__name { color: var(--ink); }

/* The old absolutely-positioned rule for `.hx-wfall__floor` was here, and it
   outlived the markup it belonged to. When the baseline label moved into the
   figure's header it kept `position: absolute`, lost the plot as its
   containing block, and printed "ACTUAL DEMAND = 100" at the top of a
   completely different band. The label is in normal flow now; its only styling
   is the tertiary ink declared with the header. */

.hx-wfall__total {
  margin-top: var(--s-8);
  font-size: var(--text-lede);
  line-height: var(--leading-lede);
  color: var(--ink-secondary);
}

.hx-wfall__total strong { color: var(--ink); font-weight: var(--weight-medium); }

.hx-wfall__caption {
  margin-top: var(--s-3);
  font-size: var(--text-h3);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-h3);
  line-height: var(--leading-h3);
  color: var(--ink);
  max-width: 30ch;
}

.hx-wfall__aside {
  display: grid;
  gap: var(--s-4);
  margin-top: var(--s-12);
  padding-top: var(--s-6);
  border-top: 1px solid var(--hairline);
  max-width: var(--measure);
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

.hx-wfall__aside strong { color: var(--ink); font-weight: var(--weight-medium); }

/* The confidentiality guard. It is small, and it is never hidden: it is the
   sentence that keeps the graphic a description of a pattern rather than a
   claim about anyone's operation. */
.hx-wfall__guard {
  margin-top: var(--s-6);
  padding-left: var(--s-4);
  border-left: 2px solid var(--accent);
  font-size: var(--text-small);
  line-height: 1.6;
  font-style: italic;
  color: var(--ink-tertiary);
  max-width: var(--measure);
}

/* ============================================================================
   5 · THE FOUR-BAR WASTE CHART  (§5.3)
   ----------------------------------------------------------------------------
   Three kinds of bar in one plot, and a reader has to be able to tell them
   apart before reading a label:

     measured   solid fill
     unknown    empty frame, hatched, marked with a query
     target     dashed outline over a tint

   FILL TREATMENT IS THE DIFFERENTIATOR. Solid, hatched and dashed survive
   greyscale, print and any colour vision. The colour separation between the
   measured fill and the target outline is audited in tools/contrast.mjs as a
   backstop, not as the mechanism.

   The empty bar is full height on purpose. A bar of height zero is a
   measurement of zero, which is a different and false claim; a full-height
   empty frame is the SPACE where a number would go.
   ========================================================================= */

.hx-waste__label { color: var(--accent); margin-bottom: var(--s-8); }

.hx-waste__bars {
  display: grid;
  gap: var(--s-6);
}

.hx-waste__col { min-width: 0; }

/* The track is a plain hairline frame. It carried a hatch in the first
   version, which put the same diagonal texture in the unfilled part of a
   MEASURED bar as in the whole of the UNMEASURED one, and blunted the single
   distinction this chart exists to make. The hatch belongs to one bar kind. */
.hx-waste__bar {
  position: relative;
  display: block;
  border: 1px solid var(--hairline);
  border-radius: 2px;
  overflow: hidden;
}

.hx-waste__fill {
  position: absolute;
  left: 0;
  bottom: 0;
  background: var(--chart-measured);
  transform-origin: left;
}

/* The unknown bar has no fill at all, and says so with a mark rather than by
   being blank. Blank reads as a rendering fault; a marked empty frame reads
   as a decision. */
.hx-waste__col--unknown .hx-waste__bar {
  border-style: dashed;
  border-color: var(--chart-unknown);
  background:
    repeating-linear-gradient(
      45deg,
      color-mix(in srgb, var(--chart-unknown) 26%, transparent) 0 1px,
      transparent 1px 8px
    );
}

.hx-waste__col--unknown .hx-waste__fill { display: none; }

.hx-waste__glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  /* 28px, comfortably inside the large-text threshold. The query mark is the
     one piece of INK drawn in the unknown tone, and at 24px it would have sat
     exactly on the 3:1 boundary rather than clear of it. */
  font-size: 1.75rem;
  color: var(--chart-unknown);
}

/* The target bar is drawn, not filled: an outline over a tint reads as
   "engineered for" where a solid bar would read as "achieved". */
.hx-waste__col--target .hx-waste__bar { border-color: transparent; }

.hx-waste__col--target .hx-waste__fill {
  background: color-mix(in srgb, var(--chart-target) 16%, transparent);
  border: 1px dashed var(--chart-target);
  border-radius: 2px;
}

.hx-waste__chip {
  position: absolute;
  color: var(--chart-target);
  background: var(--canvas);
  padding: 0 var(--s-1);
}

.hx-waste__value { display: block; color: var(--ink); }

.hx-waste__col--unknown .hx-waste__value,
.hx-waste__col--target .hx-waste__value { color: var(--ink-secondary); }

.hx-waste__name {
  display: block;
  font-size: var(--text-label);
  line-height: 1.35;
  color: var(--ink-secondary);
  overflow-wrap: anywhere;
}

/* --- Narrow: horizontal rows ---------------------------------------------
   Four columns of vertical bars at 375px gives each label about 70px, and
   "With Hexa Ops, held and measured" becomes six lines of stacked words. Rows
   read at any width, and the three fill treatments are unchanged by the
   rotation, so nothing about the argument depends on the orientation. */
.hx-waste__col {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-1) var(--s-4);
}

.hx-waste__name { grid-column: 1; grid-row: 1; }
.hx-waste__value { grid-column: 2; grid-row: 1; text-align: right; }

.hx-waste__bar {
  grid-column: 1 / -1;
  grid-row: 2;
  height: 26px;
  width: 100%;
}

/* In rows the magnitude is a WIDTH. `--hx-height` is the bar's size in the
   axis's units, not a physical height, so the same value drives both
   orientations and neither can drift from the other. */
.hx-waste__fill { top: 0; width: var(--hx-height); height: 100%; }

.hx-waste__chip { right: var(--s-2); top: 50%; transform: translateY(-50%); }

@media (min-width: 48em) {
  .hx-waste__bars {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--s-6);
    align-items: end;
    height: clamp(240px, 30vw, 320px);
  }

  .hx-waste__col {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    grid-template-columns: minmax(0, 1fr);
    align-items: end;
    height: 100%;
    gap: var(--s-3);
  }

  .hx-waste__value { grid-column: 1; grid-row: 1; text-align: center; }

  .hx-waste__bar {
    grid-column: 1;
    grid-row: 2;
    height: 100%;
    align-self: stretch;
  }

  .hx-waste__name { grid-column: 1; grid-row: 3; text-align: center; }

  .hx-waste__fill {
    top: auto;
    bottom: 0;
    width: 100%;
    height: var(--hx-height);
  }

  /* Inside the bar, at its foot. Sat on the bar's top edge in the first
     version, where it interrupted the dashed outline and needed an opaque
     background to stay legible: a background that then had to know which band
     the chart was on. Inside, it needs neither. */
  .hx-waste__chip {
    right: auto;
    left: 50%;
    top: auto;
    bottom: var(--s-2);
    background: transparent;
    transform: translateX(-50%);
  }
}

/* --- Legend and notes ---------------------------------------------------- */
.hx-waste__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-6);
  margin-top: var(--s-8);
  padding-top: var(--s-6);
  border-top: 1px solid var(--hairline);
}

.hx-waste__key {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--text-small);
  color: var(--ink-secondary);
}

.hx-waste__swatch {
  width: 18px;
  height: 12px;
  border-radius: 2px;
  border: 1px solid transparent;
}

.hx-waste__key--measured .hx-waste__swatch { background: var(--chart-measured); }

.hx-waste__key--unknown .hx-waste__swatch {
  border: 1px dashed var(--chart-unknown);
  background:
    repeating-linear-gradient(
      45deg,
      color-mix(in srgb, var(--chart-unknown) 26%, transparent) 0 1px,
      transparent 1px 8px
    );
}

.hx-waste__key--target .hx-waste__swatch {
  border: 1px dashed var(--chart-target);
  background: color-mix(in srgb, var(--chart-target) 16%, transparent);
}

.hx-waste__notes {
  display: grid;
  gap: var(--s-4);
  margin-top: var(--s-12);
}

@media (min-width: 64em) {
  .hx-waste__notes { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-6); }
}

.hx-waste__note {
  min-width: 0;
  padding: var(--s-4) var(--s-6);
  border-left: 1px solid var(--hairline);
  transition: border-color var(--fast) var(--ease-out);
}

.hx-waste__note dt {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2);
  color: var(--ink);
  font-weight: var(--weight-medium);
}

.hx-waste__note dd {
  margin-top: var(--s-2);
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

.hx-waste__kind {
  padding: 1px var(--s-2);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-sm);
}

.hx-waste__kind--measured { color: var(--ink); }

/* The unknown marker keeps the treatment in its BORDER and its text in a
   reading colour. `--chart-unknown` is a graphic tone at 4.14 on this ground:
   correct for a hatch and a dashed rule, under the 4.5 floor for twelve-pixel
   text. The dashed border is what says "unknown" here, not the ink. */
.hx-waste__kind--unknown {
  color: var(--ink-secondary);
  border-color: var(--chart-unknown);
  border-style: dashed;
}
.hx-waste__kind--target { color: var(--chart-target); border-color: var(--chart-target); border-style: dashed; }

/* Pointing at a bar lights its note, and the other way round. Nothing is
   revealed, because nothing was hidden. */
.hx-waste__note.is-lit { border-left-color: var(--accent); }

.hx-waste__col.is-lit .hx-waste__fill { filter: brightness(1.08); }
.hx-waste__col.is-lit .hx-waste__bar { border-color: var(--accent); }

.hx-waste__hint {
  margin-top: var(--s-6);
  color: var(--ink-tertiary);
}

/* ============================================================================
   6 · BANDS THIS ASSET HAS AND ASSET 01 DOES NOT
   ========================================================================= */

/* --- §2.3 · why the gain erodes ------------------------------------------ */
.hx-erosion {
  display: grid;
  gap: var(--grid-gap);
  margin-top: var(--s-16);
}

@media (min-width: 48em) {
  .hx-erosion { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.hx-erosion__item {
  min-width: 0;
  padding-top: var(--s-4);
  border-top: 1px solid var(--accent);
}

.hx-erosion__title {
  font-size: var(--text-h3);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-h3);
  line-height: var(--leading-h3);
  color: var(--ink);
}

.hx-erosion__body {
  margin-top: var(--s-3);
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

.hx-erosion__body strong { color: var(--ink); font-weight: var(--weight-medium); }

.hx-erosion__close {
  margin-top: var(--s-16);
  font-size: var(--text-lede);
  line-height: var(--leading-lede);
  color: var(--ink);
}

/* --- §2.4 · three numbered claims ---------------------------------------- */
.hx-claims {
  display: grid;
  gap: var(--s-12);
  margin-top: var(--s-16);
}

@media (min-width: 64em) {
  .hx-claims { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-8); }
}

.hx-claims__item {
  min-width: 0;
  padding-top: var(--s-4);
  border-top: 1px solid var(--hairline-strong);
}

.hx-claims__index { display: block; color: var(--accent); }

.hx-claims__title {
  margin-top: var(--s-3);
  font-size: var(--text-h3);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-h3);
  line-height: var(--leading-h3);
  color: var(--ink);
}

.hx-claims__body {
  margin-top: var(--s-3);
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

/* The sentence each claim exists to land. Set apart rather than bolded: a
   fourth weight in a system with a 600 ceiling is not available. */
.hx-claims__pull {
  margin-top: var(--s-4);
  padding-left: var(--s-4);
  border-left: 2px solid var(--accent);
  font-size: var(--text-body);
  line-height: 1.5;
  color: var(--ink);
}

.hx-whynow {
  margin-top: var(--s-24);
  padding-top: var(--s-8);
  border-top: 1px solid var(--hairline);
  max-width: var(--measure);
  font-size: var(--text-lede);
  line-height: var(--leading-lede);
  color: var(--ink-secondary);
}

.hx-whynow strong { color: var(--ink); font-weight: var(--weight-medium); }

/* --- §3 · who it is for --------------------------------------------------- */
.hx-defs { margin-top: var(--s-16); border-top: 1px solid var(--hairline-strong); }

.hx-defs__row {
  display: grid;
  gap: var(--s-1);
  padding-block: var(--s-4);
  border-bottom: 1px solid var(--hairline);
  min-width: 0;
}

@media (min-width: 48em) {
  .hx-defs__row {
    grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
    gap: var(--s-6);
  }
}

.hx-defs__term { color: var(--ink-tertiary); }

.hx-defs__value {
  margin: 0;
  color: var(--ink-secondary);
  line-height: 1.6;
}

.hx-icp__why {
  margin-top: var(--s-12);
  max-width: var(--measure);
  font-size: var(--text-lede);
  line-height: var(--leading-lede);
  color: var(--ink-secondary);
}

.hx-icp__why strong { color: var(--ink); font-weight: var(--weight-medium); }

.hx-dims { margin-top: var(--s-24); }

.hx-dims__headline {
  font-size: var(--text-h2);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-h2);
  line-height: var(--leading-h2);
  color: var(--ink);
  max-width: 18ch;
}

.hx-dims__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-8);
}

.hx-dims__item {
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-sm);
  color: var(--ink);
}

.hx-dims__body {
  margin-top: var(--s-6);
  max-width: var(--measure);
  color: var(--ink-secondary);
  line-height: 1.6;
}

.hx-dims__body strong { color: var(--ink); font-weight: var(--weight-medium); }

.hx-notes {
  display: grid;
  gap: var(--grid-gap);
  margin-top: var(--s-24);
}

@media (min-width: 48em) {
  .hx-notes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.hx-notes__item {
  min-width: 0;
  padding: var(--card-pad);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.hx-notes__label { color: var(--accent); margin-bottom: var(--s-3); }

.hx-notes__body {
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

.hx-notes__body + .hx-notes__body { margin-top: var(--s-3); }

.hx-notes__body strong { color: var(--ink); font-weight: var(--weight-medium); }

/* --- §4 · capabilities ---------------------------------------------------- */
.hx-compare__closing {
  margin-top: var(--s-12);
  max-width: 44ch;
  font-size: var(--text-lede);
  line-height: var(--leading-lede);
  color: var(--ink-secondary);
}

.hx-compare__closing strong { color: var(--ink); font-weight: var(--weight-medium); }

.hx-flow { margin-top: var(--s-12); }

.hx-flow__steps {
  display: grid;
  gap: var(--s-4);
  counter-reset: step;
}

@media (min-width: 64em) {
  .hx-flow__steps { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--s-4); }
}

.hx-flow__step {
  position: relative;
  min-width: 0;
  padding-top: var(--s-3);
  border-top: 1px solid var(--hairline-strong);
}

.hx-flow__index { display: block; color: var(--accent); }

.hx-flow__actor {
  display: block;
  margin-top: var(--s-2);
  font-weight: var(--weight-medium);
  color: var(--ink);
  line-height: 1.35;
}

.hx-flow__action {
  display: block;
  margin-top: 2px;
  font-size: var(--text-small);
  line-height: 1.5;
  color: var(--ink-secondary);
}

.hx-flow__rule {
  margin-top: var(--s-12);
  padding-left: var(--s-4);
  border-left: 2px solid var(--accent);
  max-width: var(--measure);
  line-height: 1.6;
  color: var(--ink-secondary);
}

.hx-flow__rule strong { color: var(--ink); font-weight: var(--weight-medium); }

/* The rule above this block is a quotation with a left border and no bottom
   margin, so without a top margin here the two collided. */
.hx-watched__label {
  color: var(--ink-tertiary);
  margin-top: var(--s-16);
  margin-bottom: var(--s-4);
}

.hx-watched {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.hx-watched__item {
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: var(--text-small);
  color: var(--ink-secondary);
}

.hx-pillars {
  display: grid;
  gap: var(--grid-gap);
  margin-top: var(--s-8);
}

@media (min-width: 48em) {
  .hx-pillars { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.hx-pillars__item {
  min-width: 0;
  padding: var(--card-pad);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.hx-pillars__title {
  font-size: var(--text-h3);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-h3);
  color: var(--ink);
}

.hx-pillars__rows { display: grid; gap: var(--s-3); margin-top: var(--s-4); }

.hx-pillars__rows li {
  padding-left: var(--s-4);
  position: relative;
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

.hx-pillars__rows li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 9px;
  width: 6px;
  height: 1px;
  background: var(--accent);
}

/* --- §5 · proof ----------------------------------------------------------- */
.hx-reading {
  display: grid;
  gap: var(--s-4);
  margin-top: var(--s-6);
  max-width: var(--measure);
}

.hx-reading__para { line-height: 1.6; color: var(--ink-secondary); }

.hx-reading__para strong { color: var(--ink); font-weight: var(--weight-medium); }

.hx-measure__close {
  margin-top: var(--s-8);
  max-width: var(--measure);
  font-size: var(--text-lede);
  line-height: var(--leading-lede);
  color: var(--ink-secondary);
}

.hx-measure__close strong { color: var(--ink); font-weight: var(--weight-medium); }

/* --- §6 · technical ------------------------------------------------------- */
.hx-tech {
  display: grid;
  gap: var(--s-8) var(--grid-gap);
  margin-top: var(--s-16);
}

@media (min-width: 48em) {
  .hx-tech { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-12) var(--s-16); }
}

.hx-tech__item { min-width: 0; padding-top: var(--s-4); border-top: 1px solid var(--hairline); }

.hx-tech__title {
  font-size: var(--text-body);
  font-weight: var(--weight-medium);
  color: var(--ink);
}

.hx-tech__body {
  margin-top: var(--s-2);
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

.hx-tech__body strong { color: var(--ink); font-weight: var(--weight-medium); }

/* --- §7 · conversion ------------------------------------------------------ */
.hx-cta .split-path { text-align: left; }

.hx-cta .placeholder { margin-top: var(--s-12); }

/* ============================================================================
   7 · ENTRY MOTION
   ----------------------------------------------------------------------------
   Every graphic on this page renders COMPLETE in CSS. The entry states below
   are declared under `.enter`, a flag `enter.js` sets on itself, so a page
   that loads `reveal.js` but not `enter.js` keeps its finished state instead
   of stranding a chart at zero height.

   Transforms and opacity only. Nothing here animates a layout property.
   ========================================================================= */

/* The cycle rail draws once, then the phases land along it. */
.enter [data-enter].hx-cycle .hx-cycle__track::before { transform: scaleY(0); }

.enter [data-enter].hx-cycle.is-in .hx-cycle__track::before {
  transform: scaleY(1);
  transition: transform var(--slow) var(--ease-out);
}

.enter [data-enter].hx-cycle .hx-cycle__phase {
  opacity: 0;
  transform: translate3d(0, 8px, 0);
}

.enter [data-enter].hx-cycle.is-in .hx-cycle__phase {
  opacity: 1;
  transform: none;
  transition: opacity var(--base) var(--ease-out), transform var(--base) var(--ease-out);
  transition-delay: calc(300ms + var(--i) * 120ms);
}

@media (min-width: 48em) {
  .enter [data-enter].hx-cycle .hx-cycle__track::before { transform: scaleX(0); transform-origin: left; }
  .enter [data-enter].hx-cycle.is-in .hx-cycle__track::before { transform: scaleX(1); }
}

/* The waterfall stacks one layer at a time, each label arriving as its bar
   lands, and the total resolving after all four. */
.enter [data-enter].hx-wfall .hx-wfall__bar { transform: scaleY(0); }

.enter [data-enter].hx-wfall.is-in .hx-wfall__bar {
  transform: scaleY(1);
  transition: transform var(--base) var(--ease-out);
  transition-delay: calc(var(--i) * 260ms);
}

.enter [data-enter].hx-wfall .hx-wfall__value,
.enter [data-enter].hx-wfall .hx-wfall__name { opacity: 0; }

.enter [data-enter].hx-wfall.is-in .hx-wfall__value,
.enter [data-enter].hx-wfall.is-in .hx-wfall__name {
  opacity: 1;
  transition: opacity var(--base) var(--ease-out);
  transition-delay: calc(var(--i) * 260ms + 180ms);
}

.enter [data-enter].hx-wfall .hx-wfall__col--total .hx-wfall__bar { transform: scaleX(0); transform-origin: left; }
.enter [data-enter].hx-wfall.is-in .hx-wfall__col--total .hx-wfall__bar { transform: scaleX(1); }

/* The waste bars grow together rather than in sequence. They are four
   independent claims, not a sequence, and staggering them would imply an
   order the data does not have. */
.enter [data-enter].hx-waste .hx-waste__fill { transform: scaleX(0); }

.enter [data-enter].hx-waste.is-in .hx-waste__fill {
  transform: none;
  transition: transform var(--slow) var(--ease-out);
  transition-delay: calc(var(--i) * 80ms);
}

@media (min-width: 48em) {
  .enter [data-enter].hx-waste .hx-waste__fill { transform: scaleY(0); transform-origin: bottom; }
  .enter [data-enter].hx-waste.is-in .hx-waste__fill { transform: none; }
}

.enter [data-enter].hx-flow .hx-flow__step { opacity: 0; transform: translate3d(0, 8px, 0); }

.enter [data-enter].hx-flow.is-in .hx-flow__step {
  opacity: 1;
  transform: none;
  transition: opacity var(--base) var(--ease-out), transform var(--base) var(--ease-out);
  transition-delay: calc(var(--i) * 90ms);
}

/* Motion off: everything is already complete, so the entry states are never
   applied at all. Two details, and the check caught both.

   IT IS `transform: none`, NOT A DURATION CHANGE. The global reduced-motion
   rule collapses every transition to 0.01ms, which does nothing here: a
   0.01ms transition out of `scaleY(0)` still STARTS at zero and still needs
   `.is-in` to arrive before it moves. Without this block a reader with motion
   reduced would see empty bars until each scrolled into view.

   IT IS `!important`, AND THAT IS LOAD-BEARING. The entry states above are
   four-part selectors (`.enter [data-enter].hx-waste .hx-waste__fill`); the
   three-part selectors here lose to them on specificity, so the first version
   of this block was inert and every bar measured zero. Matching the specificity
   of seven rules by hand is the kind of thing that stays right for one edit,
   so the intent is stated instead. */
@media (prefers-reduced-motion: reduce) {
  .enter [data-enter] .hx-cycle__track::before,
  .enter [data-enter] .hx-cycle__phase,
  .enter [data-enter] .hx-wfall__bar,
  .enter [data-enter] .hx-wfall__value,
  .enter [data-enter] .hx-wfall__name,
  .enter [data-enter] .hx-waste__fill,
  .enter [data-enter] .hx-flow__step {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* ==================== layer: hexa-light ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V2 — the light edition.
   ----------------------------------------------------------------------------
   Same page, same components, same type, same spacing, same motion. Two things
   change: the ground goes light, and the hero's texture becomes the ascii
   radar instead of the reactive-lines fan.

   THE PALETTE, as supplied:
     Nordic Indigo  #263BAA   the accent, and the radar's glyphs
     Vanilla Mist   #FFF4D6   the page ground
     black                    text
     #D9ED44                  the radar's ring, and nothing else

   TWO EDITIONS SHARE THIS CHASSIS.

     v2   Vanilla Mist is the page ground.
     v3   an off-white page ground, with Vanilla Mist demoted to the
          alternating band. Declared at the end of this file, so the two
          grounds sit next to each other rather than in separate files.

   Everything between here and there is common to both, which is why it keys
   off `[data-theme='light']` rather than off an edition number: a further
   light edition inherits the chassis without a selector edit.

   WHY THE SELECTOR IS TWO PARTS. Three stylesheets declare a palette on
   `.p-body-root` and two of them load after this one. Specificity (0,2,0)
   settles it in one place rather than depending on link order, which is the
   same reason the per-asset theme block in `themes.css` is two parts.

   ELEVATION RUNS BACK THE OTHER WAY. On the dark edition `--surface-sunken` is
   LIGHTER than the canvas, because raised means lighter on a dark ground.
   Here it is darker again. Inverting that mechanically is how an alternating
   band starts reading as a hole punched in the page.

   Every ratio below is produced by `node tools/contrast.mjs`.
   ========================================================================= */

[data-theme='light'] .p-body-root {
  /* Grounds. Vanilla Mist is the page; white is the raised surface, because on
     a light ground raised means lighter. */
  --canvas: #fff4d6;
  --surface: #ffffff;
  --surface-sunken: #f6e8c2;

  /* Ink. Black as instructed, warmed slightly through the secondary and
     tertiary tones so they sit on a cream ground rather than on top of it. */
  --ink: #0a0a0a;            /* 18.07 canvas · 19.80 surface · 16.25 sunken */
  --ink-secondary: #4a4438;  /*  8.81 canvas ·  9.65 surface ·  7.93 sunken */
  --ink-tertiary: #6b6355;   /*  5.41 canvas ·  5.93 surface ·  4.87 sunken */
  --ink-disabled: #a79c88;   /*  NON-TEXT ONLY                              */

  /* Hairlines invert to dark at low opacity. Opacity, never a lighter grey at
     full alpha: it is what lets one value sit correctly on all three grounds. */
  --hairline: rgba(10, 10, 10, 0.1);
  --hairline-strong: rgba(10, 10, 10, 0.18);

  --accent: #263baa;          /*  8.37 canvas · 9.18 surface · 7.53 sunken */
  --accent-quiet: #3a50c4;    /*  6.13 canvas · 6.72 surface · 5.52 sunken */
  --accent-contrast: #ffffff; /*  9.18 on a filled accent                  */
  /* Raised from 0.09. The same mix that read as a clear tint on near-black
     reads as flat grey over cream: a light ground gives a low-alpha colour
     almost nothing to tint. It carries the waterfall's total bar and the
     placeholder ground, and both have to read as the accent. */
  --accent-tint: rgba(38, 59, 170, 0.14);

  /* The ground at zero alpha. A gradient that fades to nothing needs the
     ground colour, not `transparent`: `transparent` is black at zero alpha and
     would drag a cream wash grey through its midpoint. */
  --canvas-fade: rgba(255, 244, 214, 0);

  /* Chart roles. A measured result is the page ink: the plainest, most solid
     thing in the plot, needing no colour to be understood. The target is the
     accent. The unmeasured tone is a graphic tone only. */
  --chart-measured: #0a0a0a;  /* 18.07 canvas · 16.25 sunken               */
  --chart-target: #263baa;    /*  8.37 canvas ·  7.53 sunken · 2.16 from ink */
  --chart-unknown: #7e796a;   /*  3.97 canvas ·  3.57 sunken · 2.11 from accent */

  /* The radar's two colours, read off this element by the script. */
  --radar-glyph: #263baa;
  --radar-ring: #d9ed44;

  /* The scroll highlight's rest and lit states, both reading colours. */
  --hl-rest: var(--ink-tertiary);
  --hl-lit: var(--ink);

  color-scheme: light;
}

/* ============================================================================
   THE ASCII RADAR
   ----------------------------------------------------------------------------
   Structurally the same as the reactive-lines band it replaces: an absolutely
   positioned canvas behind the copy, two washes in the page colour above it,
   and the copy on its own layer.
   ========================================================================= */

.hx-radar {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* Decoration. It must never intercept a click meant for a link, and it is
     aria-hidden, so nothing inside it is reachable by keyboard either. The
     script binds `pointerdown` to the canvas itself, which still receives
     events because the canvas re-enables them below. */
  pointer-events: none;
}

.hx-radar__canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* Clicking the field releases a ring. The canvas is the only thing here that
     wants a pointer, and it does nothing but draw. */
  pointer-events: auto;
}

/* EDGES. The field runs to the band boundary, where the cut reads as a hard
   line against the section below. Settled top and bottom only: the left and
   right edges meet the viewport, where an edge is expected. */
.hx-radar::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    var(--canvas) 0%,
    var(--canvas-fade) 18%,
    var(--canvas-fade) 82%,
    var(--canvas) 100%
  );
}

/* BEHIND THE COPY. This one is not decoration, it is the contrast fix.

   The rings fill whole cells at full alpha and the glyphs are drawn in the
   accent, so text laid straight over the field would be reading black on
   #263BAA at 2.16 against a 4.5 body floor. Measured: with the page colour
   washed over a solid accent cell at 0.75 alpha, body ink reaches 5.71 and the
   accent link 5.43, both clear. The wash is opaque well past the text before
   it starts to fall away, so the transition is never visible as an edge.

   `check-a11y.mjs` measures the composited result on every run rather than
   trusting this. Re-run it if the type scale, the band's padding or the
   radar's density change: all three move the ground. */
.hx-radar::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
    ellipse 64% 46% at 50% 46%,
    var(--canvas) 0%,
    var(--canvas) 44%,
    var(--canvas-fade) 100%
  );
}

/* NARROW. The copy block is seven lines at 375px against three at 1440, so an
   ellipse sized for the desktop composition leaves the last lines and the link
   sitting on open field. Measured at both widths by `check-a11y.mjs`, which is
   why this exists: the hero contrast pass ran at 1440 only until this edition,
   and a wash that fits the desktop copy is not evidence about the phone. */
@media (max-width: 47.99em) {
  .hx-radar::before {
    background: radial-gradient(
      ellipse 130% 52% at 50% 55%,
      var(--canvas) 0%,
      var(--canvas) 60%,
      var(--canvas-fade) 100%
    );
  }
}

/* ============================================================================
   WHAT THE LIGHT GROUND CHANGES ELSEWHERE
   ----------------------------------------------------------------------------
   Everything below is a consequence of the ground flipping, not a restyle.
   ========================================================================= */

/* The hairline hero rule under the accent link: on the dark edition the link
   hovers to white, which cannot happen here. It hovers to the ink instead, so
   the state change is a real change and not a second blue. */
[data-theme='light'] .p-btn--accent:hover { color: var(--ink); }

/* The primary button is indigo with a white label. Its hover inverts to ink on
   the page colour, which needs the canvas rather than white to stay warm. */
[data-theme='light'] .p-btn--primary:hover {
  background: var(--ink);
  color: var(--canvas);
}

/* The focus ring is drawn from the accent everywhere else in the system, which
   is correct on every ground here except a filled accent button. */
[data-theme='light'] .p-btn--primary:focus-visible { outline-color: var(--ink); }

/* The target bar's tint. 16% indigo reads grey on a cream ground where it read
   clearly blue on near-black: the same mix over a much lighter ground loses
   most of its hue. Raised until the bar is unmistakably the accent, which is
   half of what marks it as the target. */
[data-theme='light'] .hx-waste__col--target .hx-waste__fill {
  background: color-mix(in srgb, var(--chart-target) 30%, transparent);
}

/* The waste chart's target chip sits inside the bar and lets the tint show
   through; in rows it sits over the empty part of the track, where it needs
   the ground behind it. `--canvas` is the ground of the band it is in. */
[data-theme='light'] .hx-waste__chip { background: var(--canvas); }

@media (min-width: 48em) {
  [data-theme='light'] .hx-waste__chip { background: transparent; }
}

/* An intermediate off-white ground once lived here, keyed to a
   `data-edition` attribute this site's markup does not emit. Rules that can
   never match are not "harmless": they are the exact silent-failure class
   the token check exists for, so they were pruned when the stylesheet was
   merged. The grounds that ship are set by `[data-ground='neutral']` below. */

/* ==================== layer: hexa-v4 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V4
   ----------------------------------------------------------------------------
   Loaded after `hexa-light.css`, which it inherits the whole light chassis
   from. Four client directions, and nothing else moves.

   IT KEYS OFF `[data-ground='neutral']`, NOT OFF AN EDITION NUMBER. Scoped to
   `[data-edition='v4']` this stylesheet loaded on v5 and matched nothing, so
   that edition silently kept the cream ground it was meant to replace. Every
   rule was correct; none of them applied. Anything a later edition inherits
   has to be addressable as the thing it is.

     1  Vanilla Mist is gone. Neutral off-white throughout.
     2  The thesis opening moves under the hero, keeps the scroll wipe, and
        gets a lighter resting tone so the wipe is visible.
     3  The production cycle animates left to right, states "irreversible"
        once instead of three times, and gives that line the shimmer.
     4  The buffer stack reads beside its commentary, animates in, and the
        bars carry a gradient so the layers are told apart by more than
        position.

   Every ratio quoted is produced by `node tools/contrast.mjs`.
   ========================================================================= */

/* ============================================================================
   1 · THE NEUTRAL PALETTE
   ----------------------------------------------------------------------------
   No warm tone survives anywhere: the grounds, the inks and the chart's
   unmeasured tone were all carrying a cream cast that only made sense against
   a cream page. On a neutral ground a warmed grey reads as dirty rather than
   as considered.

   The inks are the design language's own Option A neutrals, which are already
   in the audit from the precision line. The accent and the radar are unchanged.
   ========================================================================= */

[data-ground='neutral'] .p-body-root {
  --canvas: #fafafa;
  --surface: #ffffff;
  --surface-sunken: #f0f0f1;

  --ink: #0a0a0a;            /* 18.97 canvas · 19.80 surface · 17.38 sunken */
  --ink-secondary: #52525b;  /*  7.41 canvas ·  7.73 surface ·  6.79 sunken */
  --ink-tertiary: #6b6b75;   /*  5.05 canvas ·  5.27 surface ·  4.63 sunken */
  --ink-disabled: #a1a1aa;   /*  NON-TEXT ONLY                              */

  --canvas-fade: rgba(250, 250, 250, 0);

  --chart-unknown: #74747e;  /*  4.43 canvas · 4.06 sunken · 1.98 from accent */

  /* THE SCROLL WIPE'S RESTING TONE, and the one value here that is a genuine
     judgement rather than a neutral swap.

     The direction is a lighter grey, so the wipe is visible. A lighter grey on
     a light page means LESS contrast, and the previous resting tone was the
     tertiary ink at 5.05, chosen when this paragraph was body size.

     It is not body size any more. Moved under the hero it is set at 24px and
     up, which is large text, and the floor for large text is 3.0 rather than
     4.5. #7C7C86 measures 3.96 on this canvas: comfortably clear of that floor
     and clearly lighter than the tone it replaces, so the sweep from resting to
     full ink is a real change rather than a half-step.

     If this paragraph is ever set smaller again, this value has to go back.
     The audit carries it as `large text` for exactly that reason. */
  --hl-rest: #7c7c86;        /*  3.96 canvas — LARGE TEXT ONLY */
  --hl-lit: var(--ink);      /* 18.97 canvas                   */
}

/* ============================================================================
   2 · THE SHIMMER
   ----------------------------------------------------------------------------
   A band of accent travelling through ink text. The dark editions sweep white
   through the accent, which on a light ground would be a sweep of invisibility;
   here both ends of the sweep are audited reading colours (ink 18.97, accent
   8.79), so the line never dips in contrast while it moves.

   `color: transparent` is declared ONLY inside the `@supports` block. Outside
   it that one declaration would erase the sentence, and the sentence is the
   constraint the whole graphic exists to state.
   ========================================================================= */

.hx-shimmer {
  color: var(--ink);
  font-weight: var(--weight-medium);
}

@supports (background-clip: text) or (-webkit-background-clip: text) {
  .hx-shimmer {
    background-image: linear-gradient(
      100deg,
      var(--ink) 0%,
      var(--ink) 40%,
      var(--accent) 50%,
      var(--ink) 60%,
      var(--ink) 100%
    );
    background-size: 300% 100%;
    background-position: 100% 0;
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }

  .js .hx-shimmer { animation: hx-shimmer 5.5s var(--ease-out) 0.8s infinite; }
}

@keyframes hx-shimmer {
  0% { background-position: 130% 0; }
  /* The sweep occupies the first third of the cycle and the rest is rest. A
     continuous sweep on a page this quiet reads as a defect, and a tight fast
     one reads as a loading skeleton. */
  32%, 100% { background-position: -30% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .js .hx-shimmer { animation: none; }
}

/* ============================================================================
   3 · THE OPENING STATEMENT, UNDER THE HERO
   ----------------------------------------------------------------------------
   Set between lede and h2: large enough that the wipe reads as a deliberate
   effect rather than a colour bug, small enough that it does not compete with
   the hero it follows.
   ========================================================================= */

.hx-ledeband { padding-block: clamp(var(--s-16), 12vh, var(--s-32)); }

.hx-ledeband__line {
  max-width: 30ch;
  font-size: clamp(1.5rem, 1.15rem + 1.5vw, 2.25rem);
  line-height: 1.32;
  letter-spacing: var(--tracking-h3);
  font-weight: var(--weight-regular);
  text-wrap: pretty;
}

/* ============================================================================
   4 · THE CYCLE, ANIMATED LEFT TO RIGHT
   ----------------------------------------------------------------------------
   The rail draws in one direction, the phases land along it in order, and a
   pulse runs the same way once each has arrived. Direction is the content
   here: the whole point of the graphic is that the sequence only goes one way.
   ========================================================================= */

.hx-cycle__constraint {
  margin-top: var(--s-12);
  max-width: 34ch;
  font-size: var(--text-h3);
  line-height: var(--leading-h3);
  letter-spacing: var(--tracking-h3);
}

/* The caption is superseded by the constraint line on this edition. Left in
   the component so the earlier editions keep it. */
[data-ground='neutral'] .hx-cycle__caption { display: none; }

/* Each node fills as it lands, rather than sitting drawn from the start. */
[data-ground='neutral'] .hx-cycle__node {
  background: var(--canvas);
  border-color: var(--accent);
}

.enter [data-enter].hx-cycle.is-in .hx-cycle__node {
  animation: hx-node-land var(--base) var(--ease-out) both;
  animation-delay: calc(340ms + var(--i) * 160ms);
}

@keyframes hx-node-land {
  from { background: var(--canvas); transform: scale(0.6); }
  60%  { background: var(--accent); transform: scale(1.25); }
  to   { background: var(--accent); transform: scale(1); }
}

/* A single pulse travelling the rail, once, after the last phase has landed.
   It runs left to right at every width, including the vertical layout below
   48em, where "left to right" becomes "top to bottom" and the meaning holds. */
@media (min-width: 48em) {
  .hx-cycle__track { position: relative; }

  .enter [data-enter].hx-cycle.is-in .hx-cycle__track::after {
    content: '';
    position: absolute;
    inset: 5px auto auto 0;
    height: 1px;
    width: 22%;
    background: linear-gradient(
      90deg,
      var(--canvas-fade),
      var(--accent),
      var(--canvas-fade)
    );
    animation: hx-rail-pulse 2.8s var(--ease-in-out) 1.1s 2 both;
  }
}

@keyframes hx-rail-pulse {
  from { transform: translateX(-30%); opacity: 0; }
  15%  { opacity: 1; }
  85%  { opacity: 1; }
  to   { transform: translateX(460%); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .enter [data-enter].hx-cycle.is-in .hx-cycle__node {
    animation: none;
    background: var(--accent);
  }
  .enter [data-enter].hx-cycle.is-in .hx-cycle__track::after { display: none; }
}

/* ============================================================================
   5 · THE BUFFER STACK, BESIDE ITS COMMENTARY
   ----------------------------------------------------------------------------
   The plot took the full width and the commentary sat under it, which put four
   short lines of text a screen away from the thing they explain. Side by side,
   the reader can hold both at once, which is the whole argument of the
   graphic: each layer was rational, the sum was not.
   ========================================================================= */

@media (min-width: 64em) {
  .hx-wfall--split {
    display: grid;
    grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
    column-gap: var(--s-16);
    align-items: start;
  }

  /* Asymmetric, not 6/6: the plot needs the width and an even split would read
     as a template. The header spans both so the axis label and the baseline
     still sit above the whole figure. */
  .hx-wfall--split .hx-wfall__head { grid-column: 1 / -1; }
  .hx-wfall--split .hx-wfall__plot { grid-column: 1; margin-bottom: 64px; }
  .hx-wfall--split .hx-wfall__read { grid-column: 2; min-width: 0; }
}

.hx-wfall__read { min-width: 0; }

.hx-wfall--split .hx-wfall__total { margin-top: 0; }

.hx-wfall--split .hx-wfall__aside { margin-top: var(--s-8); }

/* --- Gradient bars -------------------------------------------------------
   Each layer is drawn from the accent to a lighter step of it, and the mix
   deepens with the layer's index. Position already tells the layers apart;
   this makes them separable at a glance without adding a second hue, which
   would imply a category the data does not have.

   The value labels and the axis are unchanged, so nothing here is load
   bearing: strip the gradients and the chart still reads exactly the same. */
[data-ground='neutral'] .hx-wfall__col--layer .hx-wfall__bar {
  background: linear-gradient(
    170deg,
    color-mix(in srgb, var(--accent) 92%, #ffffff),
    var(--accent)
  );
  opacity: calc(0.78 + var(--i) * 0.075);
}

[data-ground='neutral'] .hx-wfall__col--total .hx-wfall__bar {
  background: linear-gradient(
    170deg,
    color-mix(in srgb, var(--accent) 18%, transparent),
    color-mix(in srgb, var(--accent) 8%, transparent)
  );
  border: 1px solid var(--accent);
  opacity: 1;
}

/* --- The sheen -----------------------------------------------------------
   One pass of light across the plot after the bars have landed. Decoration,
   and the only thing on this page that is: it carries no state and says
   nothing.

   IT IS CLIPPED BY THE GRIDLINE LAYER, NOT BY THE PLOT. The first version put
   `overflow: hidden` on the plot, which is the obvious place and the wrong
   one: the column names are positioned just below the plot's box, so the
   sheen's clip took every axis label off the chart. That is the second time
   `overflow: hidden` has hidden real content on this project, and there is now
   a check for it.

   The gridline layer is absolutely positioned to the plot's exact bounds,
   holds nothing but tick lines, and is already aria-hidden, so it clips the
   sheen to the same rectangle without clipping anything that matters. */
.hx-wfall__grid { overflow: hidden; }

.enter [data-enter].hx-wfall.is-in .hx-wfall__grid::after {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 42%;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent,
    color-mix(in srgb, var(--accent) 12%, transparent),
    transparent
  );
  animation: hx-sheen 1.5s var(--ease-out) 1.3s 1 both;
}

@keyframes hx-sheen {
  from { transform: translateX(-110%); }
  to { transform: translateX(280%); }
}

@media (prefers-reduced-motion: reduce) {
  .enter [data-enter].hx-wfall.is-in .hx-wfall__grid::after { display: none; }
}

/* ============================================================================
   6 · WHAT THE NEUTRAL GROUND CHANGES
   ========================================================================= */

/* The target bar's tint was raised twice for a cream ground. On a neutral one
   the accent tints cleanly again, so it comes back down: at 30% over off-white
   it was reading as a solid bar rather than as an outline over a wash, which is
   the distinction that marks it a target rather than a result. */
[data-ground='neutral'] .hx-waste__col--target .hx-waste__fill {
  background: color-mix(in srgb, var(--chart-target) 18%, transparent);
}

/* ==================== layer: hexa-v5 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V5
   ----------------------------------------------------------------------------
   Loaded after `hexa-v4.css`, whose neutral palette and motion it inherits
   whole. Nothing about the ground, the type or the charts' semantics changes
   here. What changes is what is on the page and how four things read.

     1  The buffer stack's commentary is one sentence in a card.
     2  The total bar runs a gradient, on purpose and continuously.
     3  The turn is one bold line.
     4  The ICP profile is a card.

   Two bands are gone (erosion, advantage) and one marker is gone (the unfilled
   destination). Those are template decisions, not style ones, and what each
   costs is recorded in CHOICES.md §32.
   ========================================================================= */

/* ============================================================================
   1 · THE CALLOUT
   ----------------------------------------------------------------------------
   One sentence carrying what three blocks carried. It gets a card and a rule in
   the accent so it reads as the conclusion drawn from the plot beside it rather
   than as a caption under it.

   The sentence ends on the confidentiality boundary: a pattern found, not a
   claim about any operation today. That clause is why the block was compressed
   rather than cut, and it is the reason this card is quiet: an emphatic
   treatment on a sentence whose job is to limit a claim would be working
   against the sentence.
   ========================================================================= */

.hx-wfall__callout {
  position: relative;
  margin-top: var(--s-8);
  padding: var(--s-6);
  border: 1px solid var(--hairline);
  border-left: 2px solid var(--accent);
  border-radius: var(--radius-md);
  background: var(--surface);
  overflow: hidden;
}

.hx-wfall__callout p {
  position: relative;
  z-index: 1;
  font-size: var(--text-body);
  line-height: 1.55;
  color: var(--ink-secondary);
}

.hx-wfall__callout strong {
  color: var(--ink);
  font-weight: var(--weight-medium);
}

/* One pass of light across the card as it arrives, then nothing. It is the
   only decoration in this band and it runs once: a card that keeps glinting
   next to a chart competes with the chart. */
.enter [data-enter].hx-wfall.is-in .hx-wfall__callout::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 40%;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent,
    color-mix(in srgb, var(--accent) 14%, transparent),
    transparent
  );
  animation: hx-callout-sheen 1.4s var(--ease-out) 1.6s 1 both;
}

@keyframes hx-callout-sheen {
  from { transform: translateX(-110%); }
  to { transform: translateX(300%); }
}

@media (prefers-reduced-motion: reduce) {
  .enter [data-enter].hx-wfall.is-in .hx-wfall__callout::before { display: none; }
}

/* ============================================================================
   2 · THE TOTAL BAR, RUNNING
   ----------------------------------------------------------------------------
   The total is the point of the graphic: four rational decisions summing to
   something nobody chose. It is the one element on this page given a
   continuous animation, and it is given one because a reader who looks at
   nothing else should look at this.

   The gradient runs THROUGH the bar rather than flashing it. Nothing about the
   bar's value, height or label depends on the animation: with motion reduced,
   with scripts blocked, or with JavaScript off, it is a bar of exactly the same
   size in exactly the same place.
   ========================================================================= */

/* KEYED TO THE FILE, NOT THE EDITION. This stylesheet loads only on v5 and
   up, so the load itself already says which editions these rules are for.
   The first version keyed them to `[data-edition='v5']` as well, and that
   qualifier silently killed the gradient on every edition after v5: same
   bug as the neutral ground, third occurrence. `:root` keeps the original
   specificity without naming an edition. */
:root .hx-wfall__col--total .hx-wfall__bar {
  background: linear-gradient(
    100deg,
    color-mix(in srgb, var(--accent) 10%, transparent) 0%,
    color-mix(in srgb, var(--accent) 34%, transparent) 26%,
    color-mix(in srgb, var(--accent) 62%, transparent) 42%,
    color-mix(in srgb, var(--accent) 34%, transparent) 58%,
    color-mix(in srgb, var(--accent) 10%, transparent) 100%
  );
  background-size: 260% 100%;
  background-position: 100% 0;
  border: 1px solid var(--accent);
  opacity: 1;
}

/* `.js` and `data-edition` are both on <html>, so this is one compound
   selector and not a descendant one. Written as a descendant it matches
   nothing, which is how the first version of this rule shipped a bar that
   carried a gradient and never moved. */
:root.js .hx-wfall__col--total .hx-wfall__bar {
  animation: hx-total-run 3.6s linear 1.2s infinite;
}

@keyframes hx-total-run {
  from { background-position: 130% 0; }
  to { background-position: -30% 0; }
}

@media (prefers-reduced-motion: reduce) {
  :root .hx-wfall__col--total .hx-wfall__bar {
    animation: none;
    background-position: 50% 0;
  }
}

/* ============================================================================
   3 · THE TURN, ONE LINE
   ----------------------------------------------------------------------------
   The premise is dropped and the call stands alone. Set bold and held on one
   line: `text-wrap: nowrap` would overflow a narrow viewport, so the line is
   allowed to wrap below the width where it stops fitting and the size steps
   down with it.
   ========================================================================= */

.hx-turn--solo .p-turn__line--shimmer {
  font-size: clamp(1.35rem, 0.9rem + 2vw, 2.5rem);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-h2);
  max-width: none;
}

@media (min-width: 48em) {
  .hx-turn--solo .p-turn__line--shimmer { white-space: nowrap; }
}

/* ============================================================================
   4 · THE ICP PROFILE, AS A CARD
   ========================================================================= */

.hx-defs--card {
  margin-top: var(--s-16);
  padding: var(--s-8) var(--card-pad);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.hx-defs--card .hx-defs__row:last-child { border-bottom: 0; padding-bottom: 0; }

.hx-defs--card .hx-defs__row:first-child { padding-top: 0; }

@media (min-width: 48em) {
  .hx-defs--card { padding: var(--s-12) var(--s-16); }
}

/* ==================== layer: hexa-v6 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V6 — the hero, rebuilt.
   ----------------------------------------------------------------------------
   Loaded after `hexa-v5.css`, which it inherits everything from. Only the hero
   band changes.

   THE BACKGROUND IS GONE. No ascii radar, no wash, no texture: the band is the
   page colour. What sits in it is a particle sphere in the middle, the copy in
   the bottom-left corner, and two badges opposite.

   THE COPY AND THE SPHERE SHARE A BAND BUT NOT A RECTANGLE, which is the whole
   reason this needs no contrast wash. The sphere is centred and sized off the
   band's smaller side; the copy sits below it in the corner. Where the two get
   close, at short viewports and at 375px, the sphere moves and shrinks rather
   than the copy being pushed onto it. `check-a11y.mjs` measures the composited
   result at both widths on every run, because "they do not overlap" is a claim
   about a layout that reflows.
   ========================================================================= */

.hx-hero6 {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  --sphere-colour: var(--accent);

  /* The nav is sticky and takes 64px of flow above this band. The other heroes
     centre their copy, so a band 64px taller than the viewport costs them
     nothing; this one puts content in the BOTTOM corner, where those 64px push
     the badges off the screen entirely. Measured: the badges' bottom edge sat
     at 967 in a 950px viewport. */
  --hx-nav-h: 64px;
}

/* The band itself, for the same 64px. Without this the section still reserves
   a full viewport below a nav that already took its share, leaving a strip of
   empty page under the badges and pushing the next band down by exactly the
   height of the bar. */
.hx-hero6.p-hero--typographic {
  min-height: calc(100svh - var(--hx-nav-h));
  min-height: calc(100dvh - var(--hx-nav-h));
}

/* --- The sphere ----------------------------------------------------------
   Centred on the band, square, and sized from the smaller side so it stays a
   sphere rather than an ellipse of empty canvas. */
.hx-sphere {
  position: absolute;
  /* SIZED TO CLEAR THE COPY, not to fill the band. At 720px across and centred
     the sphere's lower limb reached into the headline's box: measured 1.28
     against a 3.0 floor, on a hero whose copy and object were supposed to share
     a band but not a rectangle. The cap is the height budget above the corner
     the copy sits in, which is why the first term is in `vh`. */
  top: 41%;
  left: 50%;
  z-index: 0;
  width: min(54vh, 46vw, 540px);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  /* Decoration, and it must never intercept a click meant for a link. The
     canvas re-enables pointers for itself: it is the only thing here that
     wants them, and all it does is draw. */
  pointer-events: none;
}

.hx-sphere__canvas {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: auto;
}

/* --- The copy and the badges --------------------------------------------
   One row at the bottom of the band: copy left, badges right. Above the
   breakpoint they are a 7/5 split, so the copy gets the width and the badges
   sit against the right edge where the eye finishes. */
.hx-hero6__grid {
  position: relative;
  z-index: 1;
  display: grid;
  gap: var(--s-8);
  align-items: end;
  min-height: calc(100svh - var(--hx-nav-h));
  min-height: calc(100dvh - var(--hx-nav-h));
  padding-block: var(--s-16);
  grid-template-areas:
    '.'
    'copy'
    'badges';
  grid-template-rows: 1fr auto auto;
}

@media (min-width: 48em) {
  .hx-hero6__grid {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    grid-template-rows: 1fr auto;
    grid-template-areas:
      '. .'
      'copy badges';
    column-gap: var(--s-16);
  }
}

/* A short viewport is the one case where a full-height band hurts, and it is
   the same guard the typographic hero has carried since asset 01: below 640px
   tall the band sizes to its content instead of pushing the copy off. */
@media (max-height: 40em) {
  .hx-hero6__grid { min-height: 0; padding-block: var(--s-24); }
  .hx-sphere { width: min(52vh, 46vw, 420px); top: 40%; }
}

.hx-hero6__copy {
  grid-area: copy;
  padding-top: 0;
  padding-bottom: 0;
  max-width: 46ch;
  text-align: left;
}

/* SMALLER THAN THE OTHER EDITIONS' DISPLAY, and it has to be. Those heroes
   give the headline the full container and centre it; this one gives it the
   left seven columns of a split, and at the shared display size that is five
   lines of type climbing out of the corner it is supposed to sit in. Two lines
   is the shape this layout is for. */
.hx-hero6 .p-display {
  font-size: clamp(2.25rem, 4vw, 3.5rem);
  max-width: 15ch;
}

.hx-hero6__lede {
  margin-top: var(--heading-gap);
  color: var(--ink-secondary);
  max-width: 46ch;
}

.hx-hero6__badges-wrap { grid-area: badges; }

.hx-hero6__badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

@media (min-width: 48em) {
  .hx-hero6__badges { justify-content: flex-end; }
}

/* Pills, and the one place on this page a pill radius is used: the design
   language reserves it for tags, which is exactly what these are. */
.hx-hero6__badge {
  padding: var(--s-2) var(--s-4);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-pill);
  color: var(--ink);
  background: var(--surface);
  white-space: nowrap;
}

/* --- Narrow ---------------------------------------------------------------
   THE SPHERE'S SPACE IS RESERVED, NOT ESTIMATED. At this width the copy is a
   two-line headline, three lines of subhead and a row of badges, which is most
   of the band: a sphere placed by percentage lands on top of it, and tuning
   that percentage only moves the collision to a viewport nobody tested. It
   measured 1.26 against a 3.0 floor doing exactly that.

   So the sphere is pinned to the top of the band and the copy grid takes a
   top padding of the same size. The two cannot overlap at any height, because
   the space is real. */
@media (max-width: 47.99em) {
  .hx-hero6 { --hx-sphere-size: min(46vh, 84vw, 360px); }

  .hx-sphere {
    width: var(--hx-sphere-size);
    top: var(--s-8);
    transform: translateX(-50%);
  }

  .hx-hero6__grid {
    gap: var(--s-6);
    padding-top: calc(var(--hx-sphere-size) + var(--s-16));
    grid-template-rows: auto auto;
    grid-template-areas:
      'copy'
      'badges';
  }

  .hx-hero6__copy { max-width: none; }

  .hx-hero6__badges { justify-content: flex-start; }
}

/* Motion off: the sphere is drawn once, at rest, by the script. Nothing here
   needs to change, and nothing in this band animates in CSS. */

/* ==================== layer: hexa-v7 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V7 — the sphere goes, an image made of particles
   arrives.
   ----------------------------------------------------------------------------
   Loaded after `hexa-v6.css` and inherits its whole hero layout: copy in the
   bottom-left corner, badges opposite, the centrepiece box on the centre line
   with its space RESERVED at narrow widths rather than estimated. Only the
   occupant of that box and the band's ground change.

   THE HERO GOES WHITE, at the client's direction. White, not the page's
   off-white: the design language's near-white rule is knowingly set aside for
   this one band on an explicit instruction, and the seam where the band meets
   the off-white page reads as the hero lifting off the ground rather than as a
   mistake because the difference is deliberate on both sides. Everything on
   the band is audited against white already: it is the raised-surface ground
   the whole light line uses.
   ========================================================================= */

.hx-hero7 { background: var(--surface); }

/* --- The particle image ---------------------------------------------------
   The image is CONTAINED inside this box by the script (aspect preserved,
   scaled to 80% of the contain fit), so the padding the client asked for is
   real on all four sides regardless of the picture's aspect ratio. The box
   itself takes the sphere's placement wholesale: centre line at desktop, and
   at narrow widths the same reserved-space arrangement, because the collision
   that pattern prevents does not care what is drawn in the box.

   WIDER THAN THE SPHERE'S BOX, because a photograph is a landscape rectangle
   where the sphere was a circle. The height term still leads, since height is
   what the copy in the corner actually competes for. */
.hx-pimage {
  position: absolute;
  top: 41%;
  left: 50%;
  z-index: 0;
  width: min(88vh, 60vw, 760px);
  height: min(52vh, 46vw, 480px);
  transform: translate(-50%, -50%);
  /* Decoration. It must never intercept a click meant for a link; the canvas
     re-enables pointers for itself, and all it does is draw. */
  pointer-events: none;
}

.hx-pimage__canvas {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: auto;
}

/* The sphere's placement rules target `.hx-sphere`, which v7 does not render,
   so nothing needs undoing. The narrow-width reservation below mirrors the
   v6 block exactly, against this box's own height. */
@media (max-width: 47.99em) {
  .hx-hero7 { --hx-pimage-h: min(38vh, 60vw, 300px); }

  .hx-pimage {
    width: min(92vw, 480px);
    height: var(--hx-pimage-h);
    top: var(--s-8);
    transform: translateX(-50%);
  }

  .hx-hero7 .hx-hero6__grid {
    padding-top: calc(var(--hx-pimage-h) + var(--s-16));
  }
}

@media (max-height: 40em) {
  .hx-pimage { height: min(44vh, 340px); width: min(70vw, 560px); top: 38%; }
}

/* ==================== layer: hexa-v8 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V8
   ----------------------------------------------------------------------------
   Loaded after `hexa-v7.css`. Five client directions:

     1  The particle image becomes the hero's full BACKGROUND, at 120% of the
        contain fit, with the copy and the badges on top of it.
     2  The opening statement merges into the cycle band, set wider.
     3  The constraint line is reworded and set small.
     4  The close splits into two statements.
     5  The turn becomes the close's own final sentence.

   Copy changes live in the content file; this file carries the layout.
   ========================================================================= */

/* ============================================================================
   1 · THE HERO BACKGROUND
   ----------------------------------------------------------------------------
   The image box stops being a reserved rectangle and becomes the band itself.
   That retires v7's contain-with-padding geometry AND the reserved-space
   arrangement at narrow widths, because there is no longer a box for the copy
   to stay out of: the copy sits on the field, which is the instruction.

   WHAT KEEPS THE COPY LEGIBLE IS A WASH, NOT LUCK. The picture cannot be seen
   from this environment (§34), so no tuning of its placement can be trusted to
   keep ink off its darkest pixels. The band's own colour is therefore washed
   up from the bottom edge, opaque under the copy row and gone by mid-band,
   exactly the technique the radar hero used and the reason it survived audit.
   `check-a11y.mjs` measures the composited result at both widths; in the wild
   the wash guarantees the floor regardless of what the CDN serves.
   ========================================================================= */

.hx-hero8 { --hx-hero8-fade: rgba(255, 255, 255, 0); }

.hx-hero8 .hx-pimage {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transform: none;
}

/* The wash. Sits above the canvas and below the copy layer, so it can never
   tint the text and the field can never reach it. */
.hx-hero8 .hx-pimage::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* OPAQUE FROM 58% DOWN, and that number is a guarantee rather than a
     taste. The picture cannot be seen from this environment, so no placement
     of the copy can be trusted against its darkest pixels; the copy row
     always sits in the lower ~40% of the band, so the wash is fully the
     band's own colour before the copy can begin, at every viewport. The
     check measures the composite anyway; this is what makes the measurement
     hold for an image nobody here has seen. */
  background: linear-gradient(
    180deg,
    var(--hx-hero8-fade) 0%,
    var(--hx-hero8-fade) 34%,
    var(--surface) 58%,
    var(--surface) 100%
  );
}

/* V7's narrow-width reservation is retired with the box it reserved space
   for: the grid goes back to its own padding, and the copy simply sits on the
   washed field like it does at every other width. */
@media (max-width: 47.99em) {
  .hx-hero8 .hx-pimage {
    width: 100%;
    height: 100%;
    top: 0;
    transform: none;
  }

  .hx-hero8 .hx-hero6__grid { padding-top: var(--s-16); }

  /* The copy stack is far taller here (a two-line headline, six lines of
     subhead, the badge row), so its top edge climbs past the desktop wash
     line: measured, the darkest pixel under the headline was 44 where the
     floor needs white. The wash goes opaque at 38% at this width, which sits
     above the tallest copy stack any viewport here produces. */
  .hx-hero8 .hx-pimage::after {
    background: linear-gradient(
      180deg,
      var(--hx-hero8-fade) 0%,
      var(--hx-hero8-fade) 12%,
      var(--surface) 38%,
      var(--surface) 100%
    );
  }
}

@media (max-height: 40em) {
  .hx-hero8 .hx-pimage { width: 100%; height: 100%; top: 0; }
}

/* ============================================================================
   2 · THE OPENING STATEMENT, INSIDE THE CYCLE BAND
   ----------------------------------------------------------------------------
   Wider than the band it used to own: 56ch against the old 30ch. It opens the
   band and hands off to the diagram that illustrates it, so it takes the
   in-band spacing rather than a band's worth of padding.
   ========================================================================= */

.hx-cycle-lede {
  max-width: 56ch;
  margin-bottom: var(--s-16);
}

/* ============================================================================
   3 · THE CONSTRAINT, SMALL
   ----------------------------------------------------------------------------
   Down from h3 to small text at the client's direction. It keeps the shimmer:
   the sweep is what still marks it as the line the diagram exists to state,
   now that size no longer does.
   ========================================================================= */

/* `:root`, not `[data-edition='v8']`: this file loads only on v8 and up, and
   an edition qualifier here would kill the rule on every later edition, which
   is exactly what it did to v9 until the pattern was caught. */
:root .hx-cycle__constraint {
  font-size: var(--text-small);
  line-height: 1.55;
  letter-spacing: 0;
  max-width: none;
  margin-top: var(--s-8);
}

/* ============================================================================
   4 · THE CLOSE, IN TWO PARTS
   ----------------------------------------------------------------------------
   Two statements, stacked: the result, then the reason it did not hold. The
   second sits closer to the first than one band-close sits to the next, so
   they read as a pair rather than as two closes.
   ========================================================================= */

.hx-statement--second {
  margin-top: var(--s-8);
  color: var(--ink-secondary);
  font-size: clamp(1.25rem, 1rem + 1.2vw, 1.75rem);
  max-width: 36ch;
}

/* ==================== layer: hexa-v10 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V10 — the capabilities section, as cards.
   ----------------------------------------------------------------------------
   Loaded after `hexa-v8.css`. One client direction: the seven problems leave
   the four-column table and become a card grid in the reference's layout, a
   two-line header top-left, three cards on the first row, four on the second,
   each with a small animated line drawing.

   THE DRAWINGS ANIMATE IN CSS ONLY. They run with scripts blocked, and under
   reduced motion every animation stops on its finished frame: no value on any
   card depends on any of it.
   ========================================================================= */

/* --- The header ----------------------------------------------------------
   Two lines at the same size: the headline in ink, the subline in the
   tertiary tone. The reference's whole voice is that pairing. The tertiary
   ink is 5.05:1 on this ground, above the 4.5 body floor, so the size does
   not need to carry the contrast case. */
.hx-caps7__head { max-width: 42ch; }

.hx-caps7__headline,
.hx-caps7__sub {
  font-size: clamp(1.375rem, 1.05rem + 1.3vw, 1.875rem);
  line-height: 1.25;
  letter-spacing: -0.01em;
  font-weight: 600;
}

.hx-caps7__sub {
  color: var(--ink-tertiary);
  margin-top: var(--s-1);
}

/* --- The grid -------------------------------------------------------------
   Four columns. The first card starts one column in, so the top-left cell
   under the header stays empty and the three-then-four rhythm the client
   asked for falls out of the column count on its own. */
.hx-caps7__grid {
  display: grid;
  margin-top: var(--s-16);
  column-gap: var(--s-8);
  row-gap: var(--s-16);
}

@media (min-width: 64em) {
  .hx-caps7__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .hx-caps7__card:first-child { grid-column-start: 2; }
}

/* Two columns in the middle widths: the offset is retired rather than
   scaled, because with two columns an empty first cell reads as a bug. */
@media (min-width: 48em) and (max-width: 63.99em) {
  .hx-caps7__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- The card -------------------------------------------------------------
   A hairline across the top, title, method, concept, drawing. The hairline
   is the card: no box, no fill, exactly like the reference. */
.hx-caps7__card {
  border-top: 1px solid var(--hairline-strong);
  padding-top: var(--s-6);
  display: flex;
  flex-direction: column;
}

.hx-caps7__title {
  font-size: var(--text-body);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0;
  max-width: 30ch;
}

.hx-caps7__method {
  margin-top: var(--s-2);
  font-size: var(--text-small);
  line-height: 1.55;
  color: var(--ink-secondary);
  max-width: 34ch;
}

.hx-caps7__concept {
  margin-top: var(--s-3);
  font-size: var(--text-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-tertiary);
}

/* The drawing sits at the bottom of the card so the row of drawings aligns
   across cards whose text runs to different depths. */
.hx-caps7__art {
  margin-top: auto;
  padding-top: var(--s-8);
}

.hxc-svg {
  display: block;
  width: min(160px, 100%);
  height: auto;
  margin-inline: auto;
}

/* --- The strokes ---------------------------------------------------------- */
.hxc-solid {
  stroke: var(--ink);
  stroke-width: 1;
  fill: none;
}

.hxc-dash {
  stroke: var(--ink-tertiary);
  stroke-width: 1;
  stroke-dasharray: 4 5;
  fill: none;
}

.hxc-node {
  stroke: var(--ink);
  stroke-width: 1;
  fill: var(--surface);
}

.hxc-fill {
  fill: var(--accent);
  stroke: none;
}

.hxc-arrow { fill: var(--ink); stroke: none; }

/* --- The motion -----------------------------------------------------------
   Five loops, all decorative, all slow, all CSS. `transform-box: view-box`
   makes the px transform-origins below read in viewBox coordinates, which is
   the only way an SVG child can rotate about the drawing's centre. */

/* Dashes flowing along a run. One full dash period per loop, so it tiles. */
.hxc-flow { animation: hxc-dash-run 1.4s linear infinite; }

@keyframes hxc-dash-run {
  to { stroke-dashoffset: -9; }
}

/* A node breathing. */
.hxc-pulse {
  transform-box: fill-box;
  transform-origin: center;
  animation: hxc-pulse 2.8s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * 0.5s);
}

@keyframes hxc-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.12); }
}

/* The seasonal node running its ring, and the replay ring running whole. */
.hxc-orbit {
  transform-box: view-box;
  transform-origin: 80px 55px;
  animation: hxc-spin 9s linear infinite;
}

.hxc-orbit--slow { animation-duration: 14s; }

@keyframes hxc-spin {
  to { transform: rotate(360deg); }
}

/* The layer leans, drifting onto the centre line and back off it. Each row
   travels from its own offset to centre, so each gets its own keyframes. */
.hxc-lean-a,
.hxc-lean-b,
.hxc-lean-c {
  animation: 3.2s ease-in-out infinite alternate;
}

.hxc-lean-a { animation-name: hxc-lean-a; }
.hxc-lean-b { animation-name: hxc-lean-b; animation-delay: 0.4s; }
.hxc-lean-c { animation-name: hxc-lean-c; animation-delay: 0.8s; }

@keyframes hxc-lean-a {
  to { transform: translateX(46px); }
}

@keyframes hxc-lean-b {
  to { transform: translateX(-38px); }
}

@keyframes hxc-lean-c {
  to { transform: translateX(28px); }
}

/* The correction travelling its run: in on the dashed side, a beat at the
   gate, out on the solid side. */
.hxc-travel { animation: hxc-travel 4.2s ease-in-out infinite; }

@keyframes hxc-travel {
  0% { transform: translateX(0); opacity: 0; }
  10% { opacity: 1; }
  42% { transform: translateX(64px); }
  56% { transform: translateX(64px); }
  88% { transform: translateX(124px); opacity: 1; }
  100% { transform: translateX(124px); opacity: 0; }
}

/* The fitted outcomes, breathing in a stagger. */
.hxc-twinkle {
  animation: hxc-twinkle 2.6s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * 0.22s);
}

@keyframes hxc-twinkle {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 1; }
}

/* Motion off: every drawing holds its finished frame. The travelling dot
   would otherwise freeze mid-run at 0 opacity's neighbour, so it is parked
   visible at the gate. */
@media (prefers-reduced-motion: reduce) {
  .hxc-flow,
  .hxc-pulse,
  .hxc-orbit,
  .hxc-lean-a,
  .hxc-lean-b,
  .hxc-lean-c,
  .hxc-travel,
  .hxc-twinkle {
    animation: none !important;
  }

  .hxc-travel { transform: translateX(64px); opacity: 1; }
  .hxc-twinkle { opacity: 1; }
}

/* ==================== layer: hexa-v11 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V11
   ----------------------------------------------------------------------------
   Loaded after `hexa-v10.css`, whose stroke classes the timeline cards reuse.
   Three client directions: the section count as a numeral (content file), the
   closing line as a full-width banner, and the weekly stepper replaced by a
   numbered hover timeline.
   ========================================================================= */

/* --- 1 · The closing banner ----------------------------------------------
   Full bleed, accent ground, white text. The pair is already in the audited
   palette: white on this accent measures 9.18:1, which clears the 4.5 body
   floor with room, bold or not. */
.hx-caps7__banner {
  margin-top: var(--s-16);
  background: var(--accent);
  padding-block: var(--s-6);
}

.hx-caps7__banner-line {
  color: #ffffff;
  text-align: center;
  font-size: var(--text-body);
  font-weight: 600;
  line-height: 1.5;
  max-width: none;
  margin: 0;
}

.hx-caps7__banner-line strong { color: #ffffff; }

/* --- 2 · The timeline -----------------------------------------------------
   The supplied component's resting state: a bordered panel, one row per
   step, number in the quiet tone, actor in ink, action right-aligned in the
   quiet tone. The panel is the content; everything below section 3 is
   decoration on top of it. */
.hx-tl {
  list-style: none;
  margin: var(--s-12) auto 0;
  padding: var(--s-2);
  max-width: 32rem;
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-md);
}

.hx-tl__item {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: var(--s-4);
  padding: var(--s-4);
  border-radius: var(--radius-sm);
  transition: background-color 200ms var(--ease-out), transform 200ms var(--ease-out);
}

.hx-tl__num {
  font-size: var(--text-micro);
  color: var(--ink-tertiary);
  min-width: 24px;
}

.hx-tl__title {
  font-weight: 500;
  font-size: var(--text-small);
  color: var(--ink);
}

.hx-tl__sub {
  margin-left: auto;
  text-align: right;
  font-size: var(--text-micro);
  color: var(--ink-tertiary);
  max-width: 22ch;
}

/* At narrow widths the action drops under the actor instead of fighting it
   for one row. */
@media (max-width: 47.99em) {
  .hx-tl__item { flex-wrap: wrap; row-gap: var(--s-1); }
  .hx-tl__sub {
    margin-left: calc(24px + var(--s-4));
    flex-basis: 100%;
    text-align: left;
    /* `flex-basis: 100%` above is what forces the wrap, and the 22ch cap
       from the base rule was quietly undoing it: a flex item may shrink
       below its basis, so a row whose actor was short enough left room for
       the action to fit beside it after all. The result was a layout that
       stacked or did not stack depending on the length of the title — five
       rows under their actors and the sixth out to the right, at one
       particular phone width. Released here so the rule means what it says
       at every width it covers. `check-a11y.mjs` sweeps 320 to 767. */
    max-width: none;
  }
}

/* --- 3 · The reveal -------------------------------------------------------
   The card sits off the panel's left edge, invisible until its row is
   pointed at, then springs in: opacity, a small scale, a 10px drift. Only on
   devices that can hover, and only where the band has the margin to show it;
   everywhere else the rule below keeps it display:none, so it can never
   widen the page or flash on a tap. */
.hx-tl__card { display: none; }

@media (hover: hover) and (min-width: 48em) {
  .hx-tl__card {
    display: block;
    position: absolute;
    left: -100px;
    top: 50%;
    z-index: 10;
    width: 80px;
    height: 110px;
    background: var(--surface);
    border: 1px solid var(--hairline-strong);
    border-radius: var(--radius-sm);
    box-shadow: 0 18px 40px rgb(10 10 10 / 0.18);
    opacity: 0;
    pointer-events: none;
    transform: translateY(-50%) scale(0.9);
    transition:
      opacity 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
      transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
      left 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
  }

  .hx-tl__item:hover { background: var(--surface-sunken); transform: translateX(4px); }

  .hx-tl__item:hover .hx-tl__card {
    opacity: 1;
    left: -90px;
    transform: translateY(-50%) scale(1);
  }
}

.hx-tl__svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* The rule the stepper used to carry, restated under the panel at the
   panel's own measure. */
.hx-tl__rule {
  margin: var(--s-6) auto 0;
  max-width: 32rem;
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
}

/* The one stroke the capability set does not have: the reviewer's mark. */
.hxc-check {
  stroke: var(--accent);
  stroke-width: 2;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Motion off: the reveal still works, it just appears in place instead of
   springing, and the rows stop sliding. */
@media (prefers-reduced-motion: reduce) {
  .hx-tl__item,
  .hx-tl__card {
    transition: none !important;
  }

  .hx-tl__item:hover { transform: none; }
}

/* ==================== layer: hexa-v12 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V12
   ----------------------------------------------------------------------------
   Loaded after `hexa-v11.css`. Four client directions, all in the weekly and
   platform subsections: the label goes and the headline centres, the
   timeline's action column sets flush left with one line per row, the watched
   measures become icon boxes on one row, and the pillars become cards with
   coloured animated panels after a supplied reference.
   ========================================================================= */

/* --- 1 · The headline, centred, without its label ------------------------ */
.hx-weekly12__head { text-align: center; }

.hx-subhead--centered { margin-inline: auto; }

/* --- 2 · The timeline, tidied ---------------------------------------------
   Three fixed columns above the breakpoint: number, actor, action. The
   action column starts where the widest actor ends, set flush left, so the
   actions read as one aligned column; the row heights hold one line because
   the v12 action copy was cut to fit (content file). Below the breakpoint
   the v11 stacking stands. */
@media (min-width: 48em) {
  .hx-tl--tidy .hx-tl__item {
    display: grid;
    grid-template-columns: 24px 11rem minmax(0, 1fr);
    align-items: baseline;
  }

  .hx-tl--tidy .hx-tl__sub {
    margin-left: 0;
    text-align: left;
    max-width: none;
    white-space: nowrap;
  }
}

/* --- 3 · The watched measures, six boxes on one row ----------------------- */
.hx-watched12 {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-8);
  padding: 0;
}

.hx-watched12__box {
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  padding: var(--s-6) var(--s-4);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-4);
}

.hx-watched12__icon {
  display: block;
  width: 28px;
  height: 28px;
}

.hxw-stroke {
  stroke: var(--ink);
  stroke-width: 1.5;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.hxw-dot { fill: var(--ink); stroke: none; }

.hx-watched12__label {
  font-size: var(--text-micro);
  line-height: 1.45;
  color: var(--ink-secondary);
}

@media (max-width: 63.99em) {
  .hx-watched12 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 47.99em) {
  .hx-watched12 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- 4 · The pillar cards -------------------------------------------------
   The reference's shape: a soft graphic panel up top, title and short
   description under it on the page's own ground. THE TINTS ARE THE SHARED
   BRAND PASTELS AND THEY ARE DECORATIVE FILLS ONLY, the one role the design
   language gives them on a light ground: no text sits on a tint, and every
   drawing rides on the white surface inside the panel. */
.hx-plat12 {
  list-style: none;
  display: grid;
  gap: var(--s-8);
  margin-top: var(--s-8);
  padding: 0;
}

@media (min-width: 48em) {
  .hx-plat12 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.hx-plat12__panel {
  border-radius: var(--radius-md);
  padding: var(--s-8) var(--s-6);
  display: grid;
  place-items: center;
}

/* The audited pastels (decorative-fill entries in contrast.mjs), each washed
   toward white so the panel reads as a soft field rather than a swatch. */
.hx-plat12__panel--periwinkle {
  background: linear-gradient(135deg, #b5baff 0%, #ffffff 52%, #9fa1ff 100%);
}

.hx-plat12__panel--sky {
  background: linear-gradient(135deg, #aee2ff 0%, #ffffff 52%, #7fc8f5 100%);
}

.hx-plat12__panel--mint {
  background: linear-gradient(135deg, #d9f9df 0%, #ffffff 52%, #aee2ff 100%);
}

.hx-plat12__mini {
  background: #ffffff;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 30px rgb(10 10 10 / 0.12);
  padding: var(--s-3);
  width: min(100%, 168px);
}

.hx-plat12__svg {
  display: block;
  width: 100%;
  height: auto;
}

.hx-plat12__title {
  margin-top: var(--s-6);
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--ink);
}

.hx-plat12__body {
  margin-top: var(--s-2);
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--ink-secondary);
  max-width: 38ch;
}

/* --- The drawings' strokes ------------------------------------------------ */
.hxp-line {
  stroke: var(--hairline-strong);
  stroke-width: 2;
  stroke-linecap: round;
}

.hxp-stroke {
  stroke: var(--ink);
  stroke-width: 1.5;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.hxp-dashed {
  stroke: var(--ink-tertiary);
  stroke-width: 1.5;
  stroke-dasharray: 3 4;
  fill: none;
}

.hxp-digit { fill: var(--accent); }

.hxp-knob {
  fill: var(--surface);
  stroke: var(--ink);
  stroke-width: 1.5;
}

/* --- The motion -----------------------------------------------------------
   Three loops, one per card, CSS only.

   The fingerprint's digits read in, one at a time, then rest. */
.hxp-digit {
  animation: hxp-digit 3.6s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * 0.25s);
}

@keyframes hxp-digit {
  0% { opacity: 0.25; }
  22%, 78% { opacity: 1; }
  100% { opacity: 0.25; }
}

/* The mark that needs both people draws itself, holds, and lifts. */
.hxp-draw {
  stroke: var(--accent);
  stroke-width: 2;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 22;
  animation: hxp-draw 3.2s ease-in-out infinite;
}

@keyframes hxp-draw {
  0% { stroke-dashoffset: 22; }
  35%, 75% { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 22; }
}

/* The knobs drift to a new setting and back: tuned, not fixed. */
.hxp-knob--0,
.hxp-knob--1,
.hxp-knob--2 {
  animation: 3.4s ease-in-out infinite alternate;
}

.hxp-knob--0 { animation-name: hxp-knob-0; }
.hxp-knob--1 { animation-name: hxp-knob-1; animation-delay: 0.5s; }
.hxp-knob--2 { animation-name: hxp-knob-2; animation-delay: 1s; }

@keyframes hxp-knob-0 {
  to { transform: translateX(-30px); }
}

@keyframes hxp-knob-1 {
  to { transform: translateX(38px); }
}

@keyframes hxp-knob-2 {
  to { transform: translateX(-18px); }
}

/* Motion off: every drawing holds a finished frame. The check rests drawn,
   the digits rest lit, the knobs rest where their markup puts them. */
@media (prefers-reduced-motion: reduce) {
  .hxp-digit,
  .hxp-draw,
  .hxp-knob--0,
  .hxp-knob--1,
  .hxp-knob--2 {
    animation: none !important;
  }

  .hxp-digit { opacity: 1; }
  .hxp-draw { stroke-dashoffset: 0; }
}

/* ==================== layer: hexa-v13 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V13 — the dimension mosaic.
   ----------------------------------------------------------------------------
   Loaded after `hexa-v12.css`. One client direction: the seven dimensions
   leave the pill row and become a staggered mosaic of hairline cells after a
   supplied reference video, with an icon square per cell, tinted fillers,
   corner marks, floating diamonds, and a hover that lifts the cell and
   slides a solid accent bar in along its edge. The two ICP notes boxes are
   removed by the template on this edition.

   THE DIVIDERS ARE THE GRID'S OWN BACKGROUND: a 1px gap over a hairline
   ground reads as a single shared rule between any two cells, which nested
   borders cannot do without doubling. The ghost cells are the page's colour
   and mask the ground where the reference leaves the block ragged.
   ========================================================================= */

.hx-mos {
  position: relative;
  margin-top: var(--s-8);
}

.hx-mos__grid {
  list-style: none;
  padding: 1px;
  margin: 0;
  display: grid;
  gap: 1px;
  background: var(--hairline-strong);
  grid-template-columns: repeat(5, minmax(0, 1fr));
  grid-template-rows: minmax(170px, auto) minmax(150px, auto) minmax(150px, auto);
  grid-template-areas:
    'gA a a b gB'
    'c  d e b f'
    'c  x g y f';
}

/* The seven content cells, in content order. */
.hx-mos__cell--1 { grid-area: a; }
.hx-mos__cell--2 { grid-area: b; }
.hx-mos__cell--3 { grid-area: c; }
.hx-mos__cell--4 { grid-area: d; }
.hx-mos__cell--5 { grid-area: e; }
.hx-mos__cell--6 { grid-area: f; }
.hx-mos__cell--7 { grid-area: g; }

.hx-mos__fill--a { grid-area: x; }
.hx-mos__fill--b { grid-area: y; }
.hx-mos__ghost--a { grid-area: gA; }
.hx-mos__ghost--b { grid-area: gB; }

.hx-mos__cell {
  position: relative;
  background: var(--surface);
  padding: var(--s-4);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--s-8);
  transition: transform 220ms var(--ease-out), box-shadow 220ms var(--ease-out);
}

/* The reference's tinted spacers and its open ground. The section behind
   this block is the sunken tone, so the ghosts wear exactly that to read as
   open ground, and the fillers mix a step of ink into the surface to read
   as the reference's shaded spacers: distinct from both the cards and the
   ground. */
.hx-mos__fill {
  background: color-mix(in srgb, var(--ink) 6%, var(--surface));
}

.hx-mos__ghost { background: var(--surface-sunken); }

/* --- The icon square ------------------------------------------------------
   One colour, all seven: the accent, with a white line glyph on it. */
.hx-mos__icon {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 4px;
  background: var(--accent);
}

.hx-mos__icon svg {
  display: block;
  width: 20px;
  height: 20px;
}

.hxm-glyph {
  stroke: #ffffff;
  stroke-width: 1.6;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.hx-mos__label {
  font-size: var(--text-lede);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}

/* --- The hover ------------------------------------------------------------
   The cell lifts over its neighbours and a solid accent bar slides in along
   its right edge, the reference's move. Pointer enhancement only: nothing
   is hidden behind it. */
.hx-mos__cell::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 12px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 220ms var(--ease-out);
}

@media (hover: hover) {
  .hx-mos__cell:hover {
    transform: translate(-4px, -4px);
    box-shadow: 0 18px 44px rgb(10 10 10 / 0.16);
    z-index: 2;
  }

  .hx-mos__cell:hover::after { transform: scaleX(1); }
}

/* --- The reference's furniture: corner marks and floating diamonds ------- */
.hx-mos__mark {
  position: absolute;
  width: 5px;
  height: 5px;
  background: var(--ink);
}

.hx-mos__mark--a { top: calc(33% - 2px); left: -2px; }

.hx-mos__mark--b { bottom: calc(33% - 2px); right: -2px; }

.hx-mos__diamond {
  position: absolute;
  width: 46px;
  height: 46px;
  background: color-mix(in srgb, var(--ink) 4%, var(--surface));
  border: 1px solid var(--hairline);
  transform: rotate(45deg);
  animation: hx-mos-float 7s ease-in-out infinite alternate;
}

.hx-mos__diamond--a { top: 12%; left: -23px; }

.hx-mos__diamond--b {
  bottom: 8%;
  right: -23px;
  animation-delay: 1.6s;
}

@keyframes hx-mos-float {
  from { transform: rotate(45deg) translate(0, 0); }
  to { transform: rotate(45deg) translate(8px, -8px); }
}

/* --- Narrower widths ------------------------------------------------------
   The stagger is a wide-canvas arrangement. Two columns in the middle band,
   one column at phone width, with the ghosts and fillers retired: a mosaic
   that has to wrap is just a grid, and a tinted spacer in a stack is a
   mistake rather than a rhythm. */
@media (max-width: 63.99em) {
  .hx-mos__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: none;
    grid-auto-rows: minmax(140px, auto);
    grid-template-areas: none;
  }

  .hx-mos__cell,
  .hx-mos__fill,
  .hx-mos__ghost {
    grid-area: auto;
  }

  .hx-mos__fill,
  .hx-mos__ghost,
  .hx-mos__diamond,
  .hx-mos__mark {
    display: none;
  }
}

@media (max-width: 47.99em) {
  .hx-mos__grid { grid-template-columns: minmax(0, 1fr); }
}

/* Motion off: the cells hold still and the bar arrives without sliding; the
   diamonds rest. */
@media (prefers-reduced-motion: reduce) {
  .hx-mos__cell,
  .hx-mos__cell::after,
  .hx-mos__diamond {
    transition: none !important;
    animation: none !important;
  }

  .hx-mos__cell:hover { transform: none; box-shadow: none; }
}

/* ==================== layer: hexa-v14 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V14 — the mosaic, lit.
   ----------------------------------------------------------------------------
   Loaded after `hexa-v13.css`. Three client directions, all on the dimension
   mosaic: a different colour per icon with a micro animation each, a
   one-by-one entry reveal, and the supplied "shiny cards" hover ported to
   the cells, with the shine as a gradient.

   Everything here keys off `.hx-mos--shiny`, the modifier the component
   emits for this edition, never off the edition number: the third
   edition-keyed selector shipped silently broken and there will not be a
   fourth.
   ========================================================================= */

/* --- 1 · The shine ring ---------------------------------------------------
   The port's geometry, exactly: the cell's own background is a radial
   gradient centred at the pointer's element-relative coordinates, and an
   inner surface covers everything but a 2px rim, so the gradient reads as a
   glowing border that follows the pointer and bleeds across a shared
   hairline onto the neighbour's rim. The original's shine was one flat
   colour; the direction asks for a gradient, so the ring runs the accent
   family out through blue to the mint pastel, every stop an audited hex. */
.hx-mos--shiny .hx-mos__cell {
  --x: -1000px;
  --y: -1000px;
  /* The original's 2px rim. At rest it wears the surface colour, so it is
     invisible and the mosaic's hairlines stay 1px; the gradient exists only
     where the pointer puts it. */
  padding: 2px;
  background-color: var(--surface);
  background-image: radial-gradient(
    300px circle at var(--x) var(--y),
    #263baa 0,
    #2f86f5 40%,
    #d9f9df 70%,
    transparent 100%
  );
}

.hx-mos__inner {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--s-8);
  width: 100%;
  height: 100%;
  padding: var(--s-4);
  background: var(--surface);
}

/* The v13 accent bar retires on this edition: the shine is the hover. The
   lift and shadow stay. */
.hx-mos--shiny .hx-mos__cell::after { content: none; }

/* --- 2 · A colour per icon ------------------------------------------------
   Seven squares, seven colours, all hexes already audited elsewhere in the
   system's palettes, all decorative: the white glyph and the label carry
   nothing that depends on which colour is which. */
.hx-mos--shiny .hx-mos__cell--1 .hx-mos__icon { background: #263baa; }
.hx-mos--shiny .hx-mos__cell--2 .hx-mos__icon { background: #5b5fc7; }
.hx-mos--shiny .hx-mos__cell--3 .hx-mos__icon { background: #2f86f5; }
.hx-mos--shiny .hx-mos__cell--4 .hx-mos__icon { background: #1d4e7a; }
.hx-mos--shiny .hx-mos__cell--5 .hx-mos__icon { background: #0f7b3d; }
.hx-mos--shiny .hx-mos__cell--6 .hx-mos__icon { background: #d98324; }
.hx-mos--shiny .hx-mos__cell--7 .hx-mos__icon { background: #e9a238; }

/* --- 3 · The micro animations ---------------------------------------------
   One quiet loop per glyph, each drawn from what its dimension is: the pin
   settles, the tiers breathe, the bars reach, the family turns, the sprout
   sways, the area breathes, the season runs its cycle. All CSS, all small,
   all stopped under reduced motion. */
.hx-mos--shiny .hx-mos__icon svg {
  transform-box: fill-box;
  transform-origin: center;
}

.hx-mos--shiny .hx-mos__cell--1 .hx-mos__icon svg {
  animation: hxm-bob 2.6s ease-in-out infinite;
}

.hx-mos--shiny .hx-mos__cell--2 .hx-mos__icon svg {
  animation: hxm-breathe 3s ease-in-out infinite;
}

.hx-mos--shiny .hx-mos__cell--3 .hx-mos__icon svg {
  animation: hxm-reach 2.8s ease-in-out infinite;
  transform-origin: center bottom;
}

.hx-mos--shiny .hx-mos__cell--4 .hx-mos__icon svg {
  animation: hxm-tilt 3.4s ease-in-out infinite;
}

.hx-mos--shiny .hx-mos__cell--5 .hx-mos__icon svg {
  animation: hxm-sway 3.2s ease-in-out infinite;
  transform-origin: center bottom;
}

.hx-mos--shiny .hx-mos__cell--6 .hx-mos__icon svg {
  animation: hxm-breathe 3.6s ease-in-out infinite;
}

.hx-mos--shiny .hx-mos__cell--7 .hx-mos__icon svg {
  animation: hxm-spin 7s linear infinite;
}

@keyframes hxm-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-1.5px); }
}

@keyframes hxm-breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.09); }
}

@keyframes hxm-reach {
  0%, 100% { transform: scaleY(1); }
  50% { transform: scaleY(1.12); }
}

@keyframes hxm-tilt {
  0%, 100% { transform: rotate(-4deg); }
  50% { transform: rotate(4deg); }
}

@keyframes hxm-sway {
  0%, 100% { transform: rotate(-5deg); }
  50% { transform: rotate(5deg); }
}

@keyframes hxm-spin {
  to { transform: rotate(360deg); }
}

/* --- 4 · The one-by-one entry ---------------------------------------------
   The cells arrive in content order, 90ms apart, when the block scrolls
   into view. Gated on `.enter` exactly like every other entry motion on
   the page: with JavaScript off nothing is ever hidden, and the animation
   runs `backwards` rather than filling forwards, so once a cell has landed
   its transform belongs to the hover again. */
.enter .hx-mos--shiny[data-enter]:not(.is-in) .hx-mos__cell {
  opacity: 0;
}

.enter .hx-mos--shiny[data-enter].is-in .hx-mos__cell {
  animation: hx-mos-in 0.55s var(--ease-out) backwards;
  animation-delay: calc(var(--i, 0) * 90ms);
}

@keyframes hx-mos-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* Motion off: cells present, icons still, shine parked at its away state
   (the script never runs), and the entry arrives without travelling. */
@media (prefers-reduced-motion: reduce) {
  .enter .hx-mos--shiny[data-enter]:not(.is-in) .hx-mos__cell {
    opacity: 1;
  }

  .enter .hx-mos--shiny[data-enter].is-in .hx-mos__cell,
  .hx-mos--shiny .hx-mos__icon svg {
    animation: none !important;
  }
}

/* ==================== layer: hexa-v15 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V15 — the dark hero.
   ----------------------------------------------------------------------------
   Loaded after `hexa-v14.css`. One client direction: the hero becomes the
   supplied reference's layout on the ported dot-matrix field. Black band,
   blue dots, and the copy in the lower-left corner: a chip, the headline,
   the subhead, and two mono uppercase actions.

   ONE DARK BAND ON A LIGHT PAGE. Everything that sits transparently on the
   field is white, and the palette's brightest stop was chosen so white body
   copy clears 4.5:1 on it; `check-a11y.mjs` measures the composited result
   under the headline and subhead at both widths, and `contrast.mjs` holds
   the static pairs for the chip and both buttons, which carry their own
   opaque grounds. With scripts blocked, or without WebGL2, the band is
   solid black and everything on it is white on black.
   ========================================================================= */

.hx-hero15 {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: #000000;
  --hx-nav-h: 64px;
}

.hx-hero15.p-hero--typographic {
  min-height: calc(100svh - var(--hx-nav-h));
  min-height: calc(100dvh - var(--hx-nav-h));
}

.hx-dotmx {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.hx-dotmx__canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* --- The copy, lower-left ------------------------------------------------- */
.hx-hero15__grid {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  min-height: calc(100svh - var(--hx-nav-h));
  min-height: calc(100dvh - var(--hx-nav-h));
  padding-block: var(--s-16);
}

/* The base hero centres its copy block and its text, at two-class
   specificity; this one belongs to the corner, so it is matched at the same
   weight from a later file. */
.hx-hero15 .hx-hero15__copy {
  margin: 0;
  padding: 0;
  max-width: 56ch;
  text-align: left;
  /* The base copy block is a centring flex column; this one keeps the
     column and moves everything to its left edge. */
  align-items: flex-start;
}

.hx-hero15 .hx-hero15__ctas { justify-content: flex-start; }

/* The chip: white mono on its own solid black, the reference's tag. */
.hx-hero15__chip {
  display: inline-block;
  padding: var(--s-1) var(--s-3);
  background: #000000;
  color: #ffffff;
  font-size: var(--text-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.hx-hero15 .p-display {
  margin-top: var(--s-4);
  color: #ffffff;
  font-size: clamp(2.5rem, 5vw, 4rem);
  max-width: 14ch;
}

.hx-hero15__lede {
  margin-top: var(--s-4);
  color: #ffffff;
  font-size: var(--text-small);
  line-height: 1.6;
  max-width: 44ch;
}

/* --- The actions ----------------------------------------------------------
   Two mono uppercase buttons, the reference's pair: one filled white, one
   dark with a hairline of the pale blue its label wears. Both carry their
   own opaque grounds, so the field beneath them never touches their
   contrast. */
.hx-hero15__ctas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-6);
}

.hx-hero15__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  font-size: var(--text-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  transition: transform 180ms var(--ease-out), opacity 180ms var(--ease-out);
}

.hx-hero15__btn--fill {
  background: #ffffff;
  color: #000000;
}

.hx-hero15__btn--ghost {
  background: #000000;
  color: #aee2ff;
  border: 1px solid rgb(174 226 255 / 0.45);
}

.hx-hero15__btn:hover { transform: translateX(2px); }

.hx-hero15__btn:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}

.hx-hero15__arrow { line-height: 1; }

@media (max-height: 40em) {
  .hx-hero15__grid { min-height: 0; padding-block: var(--s-24); }
}

@media (prefers-reduced-motion: reduce) {
  .hx-hero15__btn { transition: none; }

  .hx-hero15__btn:hover { transform: none; }
}

/* ==================== layer: hexa-v16 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V16 — the nav becomes part of the hero.
   ----------------------------------------------------------------------------
   Loaded after `hexa-v15.css`. Two client directions: the filled hero action
   goes (template), and the nav stops being a bar above the hero. It lies
   over the dark band, fully transparent, with the wordmark in white and
   bold so it reads on the field; scrolling carries it away with the hero,
   which is ordinary landing-page behaviour and what transparency implies.

   Keyed to the file's load, never to the edition number (§39's lesson).
   ========================================================================= */

/* The bar itself: out of the flow and onto the band. `is-stuck` styling is
   stood down entirely, because a bar that is absolute cannot stick and must
   not suddenly grow a surface the first time reveal.js labels it. */
.p-nav,
.p-nav.is-stuck {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  background: transparent;
  border-bottom-color: transparent;
  backdrop-filter: none;
}

/* White and bold, per the direction. The wordmark is the only thing in the
   bar, and it sits on the field's darkest region (the band's top edge runs
   the palette from black), so white on this ground is the audited pair. */
.p-nav .p-nav__mark {
  color: #ffffff;
  font-weight: 700;
}

.p-nav .p-nav__mark:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}

/* The band no longer shares the viewport with a bar above it. */
.hx-hero15 { --hx-nav-h: 0px; }

/* ==================== layer: hexa-v17 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V17
   ----------------------------------------------------------------------------
   Loaded after `hexa-v16.css`. The client's chosen typography (option G:
   Fraunces display over an Inter body, mono untouched), a bigger hero, the
   cycle band rebuilt black with a running-line loop, and four smaller
   directions: the waterfall total bar stops running, the turn sits closer
   and shimmers slower, and two eyebrows go (template).

   Everything keys off the file's load or a band's own class, never the
   edition number (§39's standing lesson).
   ========================================================================= */

/* --- 0 · The faces --------------------------------------------------------
   Self-hosted variable files, same arrangement as Geist: SIL OFL 1.1 (see
   fonts/LICENSE-Fraunces.txt, fonts/LICENSE-Inter.txt). Fraunces carries
   its optical-size axis, so display sizes sharpen on their own. */
@font-face {
  font-family: 'Fraunces';
  src: url('../fonts/Fraunces-Variable.woff2') format('woff2-variations'),
       url('../fonts/Fraunces-Variable.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Fraunces';
  src: url('../fonts/Fraunces-Italic-Variable.woff2') format('woff2-variations'),
       url('../fonts/Fraunces-Italic-Variable.woff2') format('woff2');
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: 'Inter';
  src: url('../fonts/Inter-Variable.woff2') format('woff2-variations'),
       url('../fonts/Inter-Variable.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* The body moves to Inter; mono stays Geist Mono (its own class tree). */
.p-body-root {
  font-family: 'Inter', 'Geist', system-ui, -apple-system, sans-serif;
}

/* The serif carries display and section voice: headlines, statements, the
   turn. Component titles (cards, timeline, watched boxes) stay Inter, so
   the serif marks argument rather than furniture. */
.p-display,
.p-h2,
.p-h3,
.p-statement,
.p-turn__line,
.p-cta__statement,
.hx-caps7__headline,
.hx-caps7__sub,
.hx-dims__headline,
.hx-ledeband__line {
  font-family: 'Fraunces', Georgia, 'Times New Roman', serif;
  font-weight: 550;
  letter-spacing: 0;
}

.p-display em,
.hx-ledeband__line em {
  font-style: italic;
  font-weight: 500;
}

/* --- 1 · The hero, bigger -------------------------------------------------
   Title and subtitle both step up at the client's direction. The measured
   contrast case is unchanged: white on the field's brightest stop. */
.hx-hero15 .p-display {
  font-size: clamp(3rem, 6.2vw, 5rem);
  line-height: 1.04;
  max-width: 15ch;
}

.hx-hero15__lede {
  font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
  max-width: 46ch;
}

/* --- 2 · The cycle band, black --------------------------------------------
   Colours inverted for the dark ground; every pair is audited in
   `contrast.mjs` (hx17*). The band centres its two pieces of content with
   real air above the sentence, per the direction. */
.hx-cycleband--dark {
  background: #000000;
  padding-block: var(--s-40);
}

/* The sentence: bold, white, and the wipe re-grounded for black. The rest
   tone is large-bold-text only, same rule its light twin lives under. */
.hx-cycle-lede--dark {
  margin-top: var(--s-16);
  margin-bottom: var(--s-24);
  font-weight: 700;
  --hl-rest: #8a90a4;
  --hl-lit: #ffffff;
}

/* --- The loop -------------------------------------------------------------
   One line, three phases, and a pulse that runs it end to end without
   stopping. The line itself dies away before the right edge: the gradient
   empties it and the pulse blurs out into the same nothing. */
/* Clipped, with headroom for the pulse's glow above the track: the pulse
   leaves through the faded end and can never widen the page. */
.hx-loop {
  position: relative;
  overflow: hidden;
  padding-top: 12px;
}

.hx-loop__track {
  position: relative;
  height: 2px;
  background: linear-gradient(
    90deg,
    rgb(255 255 255 / 0.4) 0%,
    rgb(255 255 255 / 0.4) 62%,
    rgb(255 255 255 / 0.12) 82%,
    transparent 96%
  );
  display: flex;
  justify-content: space-between;
  overflow: visible;
}

.hx-loop__node {
  position: relative;
  top: -3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #000000;
  border: 1px solid rgb(255 255 255 / 0.8);
}

/* The pulse: a streak of light with a soft edge, blurred, translated the
   full width and a little beyond, fading as the line does. Slow on
   purpose; the direction asks for smooth, not fast. */
.hx-loop__pulse {
  position: absolute;
  top: -5px;
  left: 0;
  width: 160px;
  height: 12px;
  border-radius: 6px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgb(174 226 255 / 0.35) 35%,
    #ffffff 70%,
    transparent 100%
  );
  filter: blur(4px);
  opacity: 0;
  pointer-events: none;
}

.js .hx-loop__pulse {
  animation: hx-loop-run 8s cubic-bezier(0.3, 0, 0.7, 1) infinite;
}

/* `left` rather than a transform, because the travel is the TRACK's width
   and only `left`'s percentage refers to it. One small blurred element on
   an 8s cycle; nothing here needs compositing. */
@keyframes hx-loop-run {
  0% { left: -12%; opacity: 0; }
  8% { opacity: 1; }
  70% { opacity: 1; filter: blur(4px); }
  100% { left: 103%; opacity: 0; filter: blur(14px); }
}

.hx-loop__phases {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-8);
  margin: var(--s-8) 0 0;
  padding: 0;
}

.hx-loop__phase {
  transition: transform 200ms var(--ease-out);
}

.hx-loop__name {
  margin: 0;
  color: #ffffff;
  font-size: var(--text-body);
  font-weight: 600;
  letter-spacing: 0;
}

.hx-loop__note {
  margin: var(--s-2) 0 0;
  color: #a9aec0;
  font-size: var(--text-small);
  line-height: 1.55;
  max-width: 26ch;
}

@media (hover: hover) {
  .hx-loop__phase:hover { transform: translateY(-3px); }

  .hx-loop__phase:hover .hx-loop__name {
    text-shadow: 0 0 18px rgb(174 226 255 / 0.8);
  }
}

@media (max-width: 47.99em) {
  .hx-loop__phases { grid-template-columns: minmax(0, 1fr); }

  .hx-loop__note { max-width: none; }
}

/* --- 3 · The waterfall total bar, still -----------------------------------
   The running gradient stops at the client's direction; the bar keeps its
   gradient fill, parked, and the band's sheen (the shimmer) is untouched.
   Same specificity shapes as the v5 rules it overrides, from a later file. */
:root.js .hx-wfall__col--total .hx-wfall__bar {
  animation: none;
  background-position: 50% 0;
}

/* --- 4 · The turn, closer and slower --------------------------------------
   Less air above "So we rebuilt it as a system.", and a slower sweep: at
   this pace the pass reads as a shine rather than a flick. */
.hx-turn--solo.p-turn { padding-top: var(--s-12); }

.hx-turn--solo .p-turn__line--shimmer {
  animation-duration: 9s;
}

@media (prefers-reduced-motion: reduce) {
  .js .hx-loop__pulse { animation: none; }

  .hx-loop__phase,
  .hx-loop__phase:hover {
    transition: none;
    transform: none;
  }
}

/* ==================== layer: hexa-v18 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V18
   ----------------------------------------------------------------------------
   Loaded after `hexa-v17.css`. Eight client directions in one round: the
   hero copy centres and takes the wordmark's left edge, the cycle band gets
   a blue crown and a darker resting tone, the loop's dots grow and align
   over their words, the problem block aligns with the waterfall, the
   waterfall recolours (grey layers, dark blue total), the callout gains an
   avatar stack and a bold header, the second close-statement steps down to
   body type, and the profile labels reveal by scramble (template + script).

   Keyed to the file's load or a block's own class, never the edition
   number (§39's standing lesson).
   ========================================================================= */

/* --- 1 · The hero copy: centred, on the wordmark's edge -------------------
   The grid joined the wide container in the template; here it centres
   vertically and the title steps up once more. */
.hx-hero15 .hx-hero15__grid { align-items: center; }

.hx-hero15 .p-display { font-size: clamp(3.25rem, 6.8vw, 5.5rem); }

/* --- 2 · The cycle band's blue crown, and a darker resting tone -----------
   The gradient lives at the top of the band and is gone before the copy
   begins, so the statement still reads on black; the wipe's resting tone
   steps down and stays above the large-bold floor (audited: hx18WipeRest). */
.hx-cycleband--dark {
  background:
    linear-gradient(180deg, rgb(30 67 216 / 0.55) 0%, rgb(30 67 216 / 0.18) 14%, transparent 32%),
    #000000;
}

.hx-cycle-lede--dark { --hl-rest: #6e7688; }

/* --- 3 · The loop's dots: bigger, over their words ------------------------
   The track becomes the same three-column grid as the phase row below it,
   so each dot sits exactly above the word it belongs to; justify-between
   geometry is retired with the flex that produced it. */
.hx-loop__track {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* The same gap as the phase row, or the columns are not the same columns
     and the second and third dots drift off their words. */
  gap: var(--s-8);
  justify-items: start;
}

.hx-loop__node {
  top: -6px;
  width: 12px;
  height: 12px;
  border-width: 1.5px;
}

@media (max-width: 47.99em) {
  /* The phases stack at this width; a horizontal dot row over a vertical
     list points at nothing, so the dots stand down with the track. */
  .hx-loop__track { display: none; }
}

/* --- 4 · The waterfall, recoloured ----------------------------------------
   Layer bars in light grey, the total in dark blue. THE GREY FILL IS
   REGISTERED AS DECORATIVE and that is defensible only because the bars do
   not carry their meaning through their fill: every bar prints its name and
   value beside it, and each keeps a 1px border in an audited graphic tone,
   so the shape's boundary clears the non-text floor even where the fill
   cannot. The total's fill is the audited dark accent. The v17 stillness
   override stands; the sheen is untouched. */
/* Matched at the v4 gradient rule's own weight ([data-ground] prefix), from
   the later file, so the grey actually lands. */
[data-ground='neutral'] .hx-wfall__col--layer .hx-wfall__bar {
  background: #d9d9de;
  border: 1px solid #6b6b75;
}

:root .hx-wfall__col--total .hx-wfall__bar,
:root.js .hx-wfall__col--total .hx-wfall__bar {
  background: #263baa;
  border: 1px solid #263baa;
  animation: none;
}

/* --- 5 · The callout's faces, and its header ------------------------------
   Three drawn person marks, monochrome ink, overlapped like a team row; on
   hover they spring apart on the supplied elastic curve. The header is the
   old caption, body-sized and bold, per the direction. */
.hx-wfall__faces {
  display: inline-flex;
  margin-bottom: var(--s-4);
}

.hx-wfall__face {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--surface-sunken);
  border: 2px solid var(--surface);
  color: #0a0a0a;
  transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.hx-wfall__face + .hx-wfall__face { margin-left: -10px; }

.hx-wfall__face svg {
  width: 20px;
  height: 20px;
  display: block;
}

@media (hover: hover) {
  .hx-wfall__callout:hover .hx-wfall__face:nth-child(1) { transform: translateX(-6px); }
  .hx-wfall__callout:hover .hx-wfall__face:nth-child(2) { transform: translateY(-4px); }
  .hx-wfall__callout:hover .hx-wfall__face:nth-child(3) { transform: translateX(6px); }
}

.hx-wfall__callout-head {
  margin: 0 0 var(--s-2);
  font-weight: 700;
}

/* --- 6 · The second close-statement, as body type ------------------------- */
.hx-statement--second {
  font-family: 'Inter', 'Geist', system-ui, sans-serif;
  font-size: var(--text-body);
  line-height: 1.6;
  font-weight: 400;
  max-width: 60ch;
}

/* --- 7 · The scramble's in-flight tone ------------------------------------
   The unlocked characters read in the quiet tone while they run, so the
   reveal is visible without ever dropping a locked character below the
   column's audited ink. */
.hx-defs__term .is-scrambling { color: var(--ink-tertiary); }

@media (prefers-reduced-motion: reduce) {
  .hx-wfall__face { transition: none; }

  .hx-wfall__callout:hover .hx-wfall__face { transform: none; }
}

/* ==================== layer: hexa-v19 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V19
   ----------------------------------------------------------------------------
   Loaded after `hexa-v18.css`. Six client directions: four faces on the
   callout with its header in full black bold, a wider and larger second
   close-statement, the section titles evened to one size, the flip hover on
   the problem titles, the closing band on the hero's own field, and a black
   footer. Keyed to the file's load or a block's class, never the edition.
   ========================================================================= */

/* --- 1 · The callout: four faces, and the header in black bold ------------ */
/* Matched inside the callout, because the callout's own paragraph rule
   carries an element in its selector and would out-rank a lone class. */
.hx-wfall__callout .hx-wfall__callout-head,
.hx-wfall__callout .hx-wfall__callout-head strong {
  color: #0a0a0a;
  font-weight: 700;
}

@media (hover: hover) {
  .hx-wfall__callout:hover .hx-wfall__face:nth-child(1) { transform: translateX(-8px); }
  .hx-wfall__callout:hover .hx-wfall__face:nth-child(2) { transform: translate(-3px, -4px); }
  .hx-wfall__callout:hover .hx-wfall__face:nth-child(3) { transform: translate(3px, -4px); }
  .hx-wfall__callout:hover .hx-wfall__face:nth-child(4) { transform: translateX(8px); }
}

/* --- 2 · The second close-statement: wider, larger ------------------------ */
.hx-statement--second {
  font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.25rem);
  max-width: 72ch;
}

/* --- 3 · One size for the section titles ----------------------------------
   The problems header and the weekly headline join "Seven dimensions. One
   forecast per cell." at the h2 scale, so the page's section voice is one
   voice. The subline keeps its quieter tone at the same size. */
.hx-caps7__headline,
.hx-caps7__sub,
.hx-subhead--centered {
  font-size: var(--text-h2);
  line-height: var(--leading-h2);
  letter-spacing: var(--tracking-h2);
}

.hx-caps7__head { max-width: 52ch; }

/* --- 4 · The flip titles --------------------------------------------------
   The supplied FlipText geometry: perspective on the wrapper, words that
   preserve 3D, characters that flip about their own X axis with the
   source's sine-staggered delays (baked per character at build time as
   `--fd`). The direction's colour move: blue while a character is away,
   black once it lands. Runs on hovering the title, once per entry. */
.hx-flip { perspective: 1000px; }

.hx-flip__word {
  display: inline-block;
  white-space: nowrap;
  transform-style: preserve-3d;
}

.hx-flip__char {
  display: inline-block;
  transform-style: preserve-3d;
}

@media (hover: hover) {
  .hx-flip:hover .hx-flip__char {
    animation: hx-flip-char 1.2s cubic-bezier(0.3, 0, 0.25, 1);
    animation-delay: var(--fd, 0s);
  }
}

@keyframes hx-flip-char {
  0% { transform: rotateX(0deg); color: inherit; }
  25% { color: var(--accent); }
  50% { transform: rotateX(180deg); color: var(--accent); }
  75% { color: var(--accent); }
  100% { transform: rotateX(360deg); color: inherit; }
}

/* --- 5 · The closing band on the hero's field -----------------------------
   Black ground, the dot-matrix behind, white copy in front, and the hero's
   ghost button as the action. The same audited bound as the hero holds the
   copy: white on the palette's brightest stop. */
.hx-cta--dark {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: #000000;
}

.hx-cta--dark > .p-container {
  position: relative;
  z-index: 1;
}

.hx-cta--dark .p-cta__statement { color: #ffffff; }

.hx-cta--dark .p-cta__lede { color: #ffffff; }

/* --- 6 · The footer, black ------------------------------------------------
   Inverted with the audited dark tones: white for names and links, the
   quiet blue-grey for everything secondary, and every hairline re-cut in
   white at low alpha. */
.p-footer {
  background: #000000;
  color: #a9aec0;
  border-color: rgb(255 255 255 / 0.16);
}

.p-footer * { border-color: rgb(255 255 255 / 0.16); }

.p-footer .p-footer__mark,
.p-footer a {
  color: #ffffff;
}

.p-footer a:hover { color: #aee2ff; }

.p-footer a:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}

.p-footer .p-footer__group-label { color: #8a90a4; }

.p-footer .p-footer__pending { color: #a9aec0; }

@media (prefers-reduced-motion: reduce) {
  .hx-flip:hover .hx-flip__char { animation: none; }
}

/* ==================== layer: hexa-v20 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V20
   ----------------------------------------------------------------------------
   Loaded after `hexa-v19.css`. Four client directions: the headline breaks
   at "You cannot pause / a living forecast." and takes the ported fluid
   effect on hover; the problems header sets in two lines on desktop; and
   the flip on the problem titles (judged wrongly executed) gives way to the
   supplied direction-hover slide.
   ========================================================================= */

/* --- 1 · The fluid headline ------------------------------------------------
   The wrapper owns the h1 and the canvas. At rest, and in every fallback
   (no WebGL, scripts blocked, reduced motion), only the h1 exists visually;
   `.is-fluid` is the script's signed statement that the canvas is drawing
   the same words, and only then does the h1 hand over its ink.
   `check-a11y.mjs` stands the class down before measuring. */
.hx-fluid {
  position: relative;
  display: block;
}

.hx-fluid__canvas {
  display: none;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.hx-fluid.is-fluid .hx-fluid__canvas { display: block; }

.hx-fluid.is-fluid h1 { color: transparent; }

/* The lines break where the client put them; nothing else may wrap. */
.hx-fluid .p-display { max-width: none; }

.hx-fluid__l1,
.hx-fluid__l2 {
  white-space: nowrap;
}

/* --- 2 · The problems header, two lines on desktop ------------------------ */
@media (min-width: 64em) {
  /* The header's own cap (sized in its base font) was the binding one and
     held the headline to three lines at any headline width; the headline's
     own measure takes over here. */
  .hx-caps7__head { max-width: none; }

  .hx-caps7__headline { max-width: 30ch; }

  .hx-caps7__sub { max-width: 44ch; }
}

/* --- 3 · The direction-hover titles ----------------------------------------
   The supplied component's geometry: three stacked copies, the base one in
   flow and an accent copy beyond each edge; entering from the top slides
   the stack down so the top accent copy arrives from above, entering from
   the bottom does the mirror, and leaving returns to base. The duplicates
   wrap exactly like the base because they share its box. The script only
   writes `data-dir`; with it absent (scripts blocked, touch, reduced
   motion) the base copy simply stands. */
.hx-dh {
  overflow: hidden;
  --hx-dh-gap: 12px;
}

.hx-dh__stack {
  position: relative;
  display: block;
  transition: transform 0.3s ease-in-out;
}

.hx-dh__copy { display: block; }

.hx-dh__copy--accent {
  position: absolute;
  inset-inline: 0;
  color: var(--accent);
}

.hx-dh__copy--above { bottom: calc(100% + var(--hx-dh-gap)); }

.hx-dh__copy--below { top: calc(100% + var(--hx-dh-gap)); }

.hx-dh[data-dir='top'] .hx-dh__stack {
  transform: translateY(calc(100% + var(--hx-dh-gap)));
}

.hx-dh[data-dir='bottom'] .hx-dh__stack {
  transform: translateY(calc(-100% - var(--hx-dh-gap)));
}

@media (prefers-reduced-motion: reduce) {
  .hx-dh__stack { transition: none; }

  .hx-dh[data-dir='top'] .hx-dh__stack,
  .hx-dh[data-dir='bottom'] .hx-dh__stack {
    transform: none;
  }
}

/* ==================== layer: hexa-v21 ==================== */
/* ============================================================================
   ASSET 03 · HEXA OPS · V21
   ----------------------------------------------------------------------------
   Loaded after `hexa-v20.css`. Five client directions: the cycle wipe
   centres with the client's near-black resting tone, the problems header
   takes the client's own four lines, the direction hover runs one direction
   for every title, the weekly timeline widens, and its reveal cards take
   the platform panels' gradients, repeated down the list.
   ========================================================================= */

/* --- 1 · The wipe, centred, resting near-black ----------------------------
   Centre to centre: the block centres in the band and the text centres in
   the block. THE RESTING TONE IS THE CLIENT'S #1F1E1E, which on this black
   band is below every reading floor: at rest a word is nearly invisible,
   and the wipe lighting it to white IS the effect now. That is defensible
   only because the dim state exists solely inside the wipe's own
   `@supports` block: browsers without scroll-driven animation, reduced
   motion, and scripts-blocked all read the paragraph fully lit. The audit
   carries the tone as a motion state, not as text (`contrast.mjs`). */
.hx-cycle-lede--dark {
  margin-inline: auto;
  text-align: center;
  --hl-rest: #1f1e1e;
}

/* --- 2 · The header's four lines ------------------------------------------
   The breaks are in the markup (the client's own); narrow screens stand
   them down and wrap naturally. The v20 measures retire: the lines are the
   measure now. */
@media (min-width: 64em) {
  .hx-caps7__headline,
  .hx-caps7__sub {
    max-width: none;
  }
}

@media (max-width: 63.99em) {
  .hx-caps7__br { display: none; }
}

/* --- 3 · One direction for every slide ------------------------------------
   Both entry edges produce the same move, at the same weight as the v20
   rules and from a later file, so the last word wins. */
.hx-dh[data-dir='top'] .hx-dh__stack,
.hx-dh[data-dir='bottom'] .hx-dh__stack {
  transform: translateY(calc(100% + var(--hx-dh-gap)));
}

@media (prefers-reduced-motion: reduce) {
  .hx-dh[data-dir='top'] .hx-dh__stack,
  .hx-dh[data-dir='bottom'] .hx-dh__stack {
    transform: none;
  }
}

/* --- 4 · The timeline, wider ---------------------------------------------- */
.hx-tl { max-width: 44rem; }

.hx-tl__rule { max-width: 44rem; }

/* --- 5 · The reveal cards, in the platform's gradients --------------------
   The same three tints the platform cards wear (hexa-v12.css), in the same
   order, repeated for the second three rows. Decorative fills only: the
   drawing rides on them in ink, and the card is hover decoration to begin
   with. */
.hx-tl__item:nth-child(3n + 1) .hx-tl__card {
  background: linear-gradient(135deg, #b5baff 0%, #ffffff 52%, #9fa1ff 100%);
}

.hx-tl__item:nth-child(3n + 2) .hx-tl__card {
  background: linear-gradient(135deg, #aee2ff 0%, #ffffff 52%, #7fc8f5 100%);
}

.hx-tl__item:nth-child(3n) .hx-tl__card {
  background: linear-gradient(135deg, #d9f9df 0%, #ffffff 52%, #aee2ff 100%);
}


/* ==================== layer: site ==================== */
/* ============================================================================
   THE SITE LAYER — fixes made after the split, on the shipping pages.
   ----------------------------------------------------------------------------
   Everything above this marker is the approved page's stylesheet, merged in
   its exact load order. Everything below exists because a check written for
   this site caught something the portfolio's checks could not see. Each fix
   names its check.
   ========================================================================= */

/* --- 1 · The headline must fit 375px (check-a11y: "no text crosses the
   viewport edge") -----------------------------------------------------------
   The fluid headline's two lines are nowrap by design ("the lines break
   where the client put them; nothing else may wrap") and the display size
   clamps to a 3.25rem floor — which at 375px is wider than the viewport, so
   the hero clipped the last letters of both lines, and stretched the copy
   column so the subhead clipped with it. Below 40em the floor gives way to
   a viewport-proportional size that keeps the longer line inside the
   viewport with margin; at 500px and up the clamp lands back on 3.25rem, so
   nothing changes at any width the approved page was reviewed at. */
@media (max-width: 40em) {
  .hx-hero15 .p-display { font-size: clamp(2rem, 10.4vw, 3.25rem); }
}

/* --- 2 · The 404's wordmark is on a light ground (check-a11y: "nav wordmark
   clears 4.5:1 on its composited ground") -----------------------------------
   The bar is transparent and the wordmark white, which is the audited pair
   over the hero's dark field. The 404 has no field, so white sat on the
   canvas at 1.04:1. The page marks itself and the wordmark takes the ink. */
/* The 404 once needed an ink wordmark, because the bar was transparent
   over its light canvas. The bar now carries its own dark ground on every
   page (see the fixed-bar block at the end of this file), so the white
   wordmark is correct here too and the override is gone. */

/* --- 3 · The defensibility grid (client round: technical redesigned) ------
   Three points each side of the isometric drawing, after the supplied
   reference: each column framed by a top and bottom rule that meet the
   diagram's corner brackets, ONE separator between adjacent points, and
   the drawing filling layer by layer. Monochrome throughout: ink for
   titles, secondary for bodies, tertiary for the glyphs and the drawing's
   wireframes.

   The fill on the planes is a CSS animation, so it survives blocked
   scripts, and it is gated on prefers-reduced-motion, where the drawing
   holds fully formed. */

.hx-def {
  display: grid;
  grid-template-areas: 'diagram' 'left' 'right';
  gap: var(--s-8);
  margin-top: var(--s-12);
}

@media (min-width: 64em) {
  .hx-def {
    grid-template-areas: 'left diagram right';
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.7fr) minmax(0, 1fr);
    gap: 0;
    margin-top: var(--s-16);
  }
}

/* The column closes with a strong rule at its very bottom (::after is a
   flex item, so space-between pins it to the extreme), level with the
   diagram's bottom corner brackets. The TOP rule was removed at the
   client's direction: the first point opens the column bare. Between
   adjacent points there is exactly ONE separator, a dedicated element
   centred in the gap. */
.hx-def__col {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--s-6);
}

.hx-def__col::after {
  content: '';
  display: block;
  height: 1px;
  background: var(--hairline-strong);
}

.hx-def__col--left { grid-area: left; }
.hx-def__col--right { grid-area: right; }

.hx-def__rule {
  display: block;
  height: 1px;
  background: var(--hairline);
}

.hx-def__item { padding: var(--s-2) 0; }

.hx-def__icon {
  display: block;
  width: 28px;
  height: 28px;
  color: var(--ink-tertiary);
  margin-bottom: var(--s-4);
}

.hx-def__icon svg { display: block; width: 100%; height: 100%; }

.hx-def__title {
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: var(--weight-semibold, 600);
  color: var(--ink);
  margin: 0 0 var(--s-2);
}

.hx-def__body {
  font-size: var(--text-small);
  line-height: 1.55;
  color: var(--ink-secondary);
  margin: 0;
  max-width: 34ch;
}

/* --- The centrepiece ------------------------------------------------------ */

.hx-def__diagram {
  grid-area: diagram;
  position: relative;
  padding: var(--s-8) var(--s-8);
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
}

.hx-blob {
  position: relative;
  width: 100%;
  max-width: 26rem;
  aspect-ratio: 1 / 1;
  overflow: hidden;
}

/* On desktop the blob's canvas fills the WHOLE bracket-framed cell, so the
   metal has the full frame to wander; the shader's zoom (data-size on the
   host) keeps the body itself at a sane scale inside it. */
@media (min-width: 64em) {
  .hx-def__diagram { padding: 0; }

  .hx-blob {
    position: absolute;
    inset: 1px;
    width: auto;
    max-width: none;
    aspect-ratio: auto;
  }
}

/* The fallback bead: a soft sphere in the page's blues, holding the centre
   until (unless) the metal renders. The script hides it with `.is-blob`. */
.hx-blob__fallback {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(56%, 15rem);
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle at 34% 28%, #7fa8ff, #2e5ae8 48%, #16277c 85%);
  box-shadow: 0 24px 60px rgba(46, 90, 232, 0.25);
}

.hx-blob.is-blob .hx-blob__fallback { display: none; }

/* Corner brackets, as the reference frames its centre cell. */
.hx-def__corner {
  position: absolute;
  width: 14px;
  height: 14px;
  color: var(--ink-tertiary);
}

.hx-def__corner--tl { top: 0; left: 0; border-top: 1px solid currentColor; border-left: 1px solid currentColor; }
.hx-def__corner--tr { top: 0; right: 0; border-top: 1px solid currentColor; border-right: 1px solid currentColor; }
.hx-def__corner--bl { bottom: 0; left: 0; border-bottom: 1px solid currentColor; border-left: 1px solid currentColor; }
.hx-def__corner--br { bottom: 0; right: 0; border-bottom: 1px solid currentColor; border-right: 1px solid currentColor; }

/* On the stacked mobile order the brackets frame the drawing alone and the
   columns read as one list. */
@media (max-width: 63.9375em) {
  .hx-def__diagram { padding: var(--s-4); }
}

/* --- 4 · The further-variables strip (client round) -----------------------
   Under the seven-dimension mosaic: the variables the grid does not
   enumerate, set QUIETLY on purpose — caption-size labels, small glyphs in
   the tertiary ink, no cells, no hover. Four to a row on desktop, matching
   how the list was given; two to a row on narrow screens. */

.hx-vars {
  margin-top: var(--s-6);
}

.hx-vars__label {
  font-size: var(--text-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin: 0 0 var(--s-4);
}

.hx-vars__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-3) var(--s-6);
}

@media (min-width: 48em) {
  .hx-vars__list { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.hx-vars__item {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
}

.hx-vars__icon {
  flex: none;
  display: block;
  width: 20px;
  height: 20px;
  color: var(--ink-tertiary);
}

.hx-vars__icon svg { display: block; width: 100%; height: 100%; }

.hx-vars__name {
  font-size: var(--text-small);
  color: var(--ink-secondary);
}

/* --- 5 · The margin stack (client rounds: one produced column, alive) ----
   Demand (dashed), the budgeted plan (grey), and ONE produced column that
   cycles between its two misses: overshooting the demand line (excess in
   the accent), sinking below it (shortfall hatched in the alert red), and
   back. The callout shows ONE explanation at a time, faded in step with
   the bar. NO quantities anywhere.

   The alert red #C0261B is audited in tools/contrast.mjs, and colour is
   never the only signal: the under state is also hatched, and both misses
   are named in words.

   THE CLOCK. Every animation here runs 12s and attaches at the same moment
   (the figure's `.is-in`), so the bar and the callout cannot drift apart.
   Windows: over holds to 36%, the bar sinks by 46%, under holds to 86%,
   and the bar climbs home. The callout crossfades inside the transitions.

   THE STATIC FALLBACK IS DELIBERATE. Scripts blocked, or reduced motion:
   nothing animates, the produced column holds its over state, and BOTH
   explanations sit in normal flow, fully lit — the animation is emphasis
   and drama, never the only carrier of the under miss. */

.hx-mstack { margin: 0; }

.hx-mstack__label {
  font-size: var(--text-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin: 0 0 var(--s-6);
}

.hx-mstack__body { display: grid; gap: var(--s-8); }

@media (min-width: 64em) {
  .hx-mstack__body {
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    gap: var(--s-16);
    align-items: stretch;
  }

  /* The over box rides HIGH, level with the tall bar; the under box sits
     LOW, level with the shortfall. space-between against the stretched
     column height does the positioning without magic offsets. */
  .hx-mstack__msgs {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: var(--s-2) 0 var(--s-8);
  }
}

.hx-mstack__plot {
  --track: 250px;
  position: relative;
}

.hx-mstack__cols {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(12px, 3vw, 34px);
}

.hx-mstack__col { min-width: 0; }

/* column-reverse stacks the segments bottom-up: the floor first, the miss
   above it. */
.hx-mstack__bar {
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-start;
  height: var(--track);
}

.hx-mstack__seg {
  display: block;
  height: calc(var(--track) * var(--hx-h) / 100);
}

.hx-mstack__seg--base {
  background: transparent;
  border: 1px dashed var(--ink-secondary);
}

.hx-mstack__seg--carry {
  background: #d9d9de;
  border: 1px solid #6b6b75;
}

/* The produced column's three parts take their geometry from the figure's
   custom properties, not --hx-h, because these are the heights the clock
   animates. Base state = the over cycle, which is what holds when nothing
   animates. */
.hx-mstack__seg--pbase {
  background: #d9d9de;
  border: 1px solid #6b6b75;
  height: calc(var(--track) * var(--hx-base) / 100);
}

.hx-mstack__seg--pover {
  background: var(--accent);
  height: calc(var(--track) * (var(--hx-over) - var(--hx-base)) / 100);
}

/* The shortfall: not production, so not a solid block — hatched in the
   alert red, open where the demand line closes it. Hidden in the base
   state (opacity, so its border cannot leave a 2px residue at height 0). */
.hx-mstack__seg--punder {
  background: repeating-linear-gradient(
    -45deg,
    rgba(192, 38, 27, 0.28) 0 4px,
    transparent 4px 8px
  );
  border: 1px dashed #c0261b;
  border-bottom: 0;
  height: 0;
  opacity: 0;
}

.hx-mstack__name {
  display: block;
  margin-top: var(--s-2);
  font-size: var(--text-small);
  font-weight: var(--weight-medium);
  color: var(--ink);
  line-height: 1.3;
}

.hx-mstack__note {
  display: block;
  font-size: var(--text-label);
  color: var(--ink-tertiary);
  line-height: 1.35;
}

/* The dashed line at actual demand's height, crossing every column.
   Measured from the PLOT'S TOP, so the labels under the bars cannot shift
   it. */
.hx-mstack__baseline-rule {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(var(--track) * (100 - var(--hx-base)) / 100);
  border-top: 1px dashed var(--ink-tertiary);
}

/* The two MESSAGE BOXES, each marked like its bar state: quiet cards with
   a soft shadow and the miss's own colour on the leading edge — subtle,
   but they arrive like a notification. In the static conditions both sit
   at their places, fully readable. */
.hx-mstack__msgs { min-width: 0; }

.hx-mstack__why {
  position: relative;
  margin: 0;
  padding: var(--s-4) var(--s-4) var(--s-4) 44px;
  background: var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: 12px;
  box-shadow: 0 12px 32px rgba(10, 10, 10, 0.08);
  font-size: var(--text-small);
  line-height: 1.55;
  color: var(--ink-secondary);
  max-width: 40ch;
}

.hx-mstack__why strong { color: var(--ink); }

.hx-mstack__why--over { border-left: 3px solid var(--accent); }

.hx-mstack__why--under { border-left: 3px solid #c0261b; }

.hx-mstack__why + .hx-mstack__why { margin-top: var(--s-4); }

.hx-mstack__mark {
  position: absolute;
  left: 16px;
  top: calc(var(--s-4) + 0.28em);
  width: 14px;
  height: 14px;
  display: block;
}

.hx-mstack__mark--over { background: var(--accent); }

.hx-mstack__mark--under {
  background: repeating-linear-gradient(
    -45deg,
    rgba(192, 38, 27, 0.28) 0 3px,
    transparent 3px 6px
  );
  border: 1px dashed #c0261b;
}

/* --- The cycle: ONE clock, ONE state -------------------------------------
   The first build ran five parallel 12s keyframe animations and trusted
   them to stay in phase; in practice they drifted and the callout ended up
   contradicting the bar. Now there is exactly one source of truth: a small
   script (scripts/margin-cycle.js) toggles `hx-mstack--under` on the
   figure every few seconds, and EVERYTHING — segment heights, box
   visibility — is a CSS transition keyed to that class. Chart and message
   share a state, not merely a schedule, so a mismatch is impossible by
   construction: whatever throttling or timing the browser applies, both
   read the same class.

   The script adds `hx-mstack--run` before cycling, so without it (scripts
   blocked) or under reduced motion (the script stands down) the static
   truth holds: the over state drawn, BOTH boxes fully lit. The outgoing
   box leaves first and the incoming one arrives on a delay — an exit,
   then an entrance. */

.hx-mstack--run .hx-mstack__seg--pbase,
.hx-mstack--run .hx-mstack__seg--pover,
.hx-mstack--run .hx-mstack__seg--punder {
  transition: height 0.9s ease-in-out, width 0.9s ease-in-out, opacity 0.5s ease;
}

.hx-mstack--run.hx-mstack--under .hx-mstack__seg--pbase {
  height: calc(var(--track) * var(--hx-under) / 100);
}

.hx-mstack--run.hx-mstack--under .hx-mstack__seg--pover {
  height: 0px;
  opacity: 0;
}

.hx-mstack--run.hx-mstack--under .hx-mstack__seg--punder {
  height: calc(var(--track) * (var(--hx-base) - var(--hx-under)) / 100);
  opacity: 1;
}

.hx-mstack--run .hx-mstack__why {
  transition: opacity 0.35s ease, transform 0.35s ease;
}

/* Default state = over: the over box on stage (arriving on a delay when it
   returns), the under box fully off. */
.hx-mstack--run .hx-mstack__why--over { transition-delay: 0.5s; }

.hx-mstack--run .hx-mstack__why--under {
  opacity: 0;
  transform: translateY(10px) scale(0.97);
  transition-delay: 0s;
}

.hx-mstack--run.hx-mstack--under .hx-mstack__why--over {
  opacity: 0;
  transform: translateY(10px) scale(0.97);
  transition-delay: 0s;
}

.hx-mstack--run.hx-mstack--under .hx-mstack__why--under {
  opacity: 1;
  transform: none;
  transition-delay: 0.5s;
}

/* NARROW SCREENS: the chart flips HORIZONTAL — bars become rows with a
   full-width label column, the demand line becomes a vertical dashed rule,
   and the clock animates widths instead of heights. */
@media (max-width: 47.9375em) {
  .hx-mstack__plot { --mlab: 6.75rem; }

  .hx-mstack__cols {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
  }

  .hx-mstack__col {
    display: grid;
    grid-template-columns: var(--mlab) minmax(0, 1fr);
    grid-template-rows: auto auto;
    column-gap: var(--s-3);
    align-items: center;
  }

  .hx-mstack__bar {
    grid-column: 2;
    grid-row: 1 / span 2;
    flex-direction: row;
    justify-content: flex-start;
    height: 18px;
    width: 100%;
  }

  .hx-mstack__seg { height: 100%; width: calc(100% * var(--hx-h) / 100); }

  .hx-mstack__seg--pbase { height: 100%; width: calc(100% * var(--hx-base) / 100); }
  .hx-mstack__seg--pover { height: 100%; width: calc(100% * (var(--hx-over) - var(--hx-base)) / 100); }
  .hx-mstack__seg--punder {
    height: 100%;
    width: 0;
    border-bottom: 1px dashed #c0261b;
    border-right: 0;
  }

  .hx-mstack__name { grid-column: 1; grid-row: 1; margin-top: 0; align-self: end; }
  .hx-mstack__note { grid-column: 1; grid-row: 2; align-self: start; }

  .hx-mstack__baseline-rule {
    left: calc(var(--mlab) + var(--s-3) + (100% - var(--mlab) - var(--s-3)) * var(--hx-base) / 100);
    right: auto;
    top: 0;
    bottom: 0;
    border-top: 0;
    border-left: 1px dashed var(--ink-tertiary);
  }
}

@media (max-width: 47.9375em) {
  /* The horizontal flip's state targets: the transitions above already
     cover width. */
  .hx-mstack--run.hx-mstack--under .hx-mstack__seg--pbase {
    height: 100%;
    width: calc(100% * var(--hx-under) / 100);
  }

  .hx-mstack--run.hx-mstack--under .hx-mstack__seg--pover {
    height: 100%;
    width: 0;
  }

  .hx-mstack--run.hx-mstack--under .hx-mstack__seg--punder {
    height: 100%;
    width: calc(100% * (var(--hx-base) - var(--hx-under)) / 100);
  }
}


/* --- 6 · Dimensions round: mosaic recolours, wider body, flip titles ------
   Four client directions in one round.

   THE SHINE goes black-and-blue: same pointer-tracked ring geometry, the
   stops swapped from the accent-to-mint run.

   THE ICON TILES go monochrome at rest — ink squares, white glyphs — and
   take the accent only while the pointer is on their cell, so colour
   becomes the hover's reward rather than the grid's noise.

   THE BODY under the mosaic runs the mosaic's own width, with air between
   it and the variables strip.

   THE DEFENSIBILITY TITLES flip character by character on hover (the
   supplied reference's sine-staggered flip, built at render time) and
   settle into the accent once the flip has run. Reduced motion skips the
   spin; the colour change, which is not motion, stays. */

.hx-mos--shiny .hx-mos__cell {
  /* 140px, down from the port's 300px: at 300 the ring bled across two or
     three neighbouring cells; at 140 it reads as the hovered card's own. */
  background-image: radial-gradient(
    140px circle at var(--x) var(--y),
    #0a0a0a 0,
    #263baa 45%,
    #2e5ae8 72%,
    transparent 100%
  );
}

.hx-mos--shiny .hx-mos__cell .hx-mos__icon {
  background: var(--ink);
  transition: background 0.25s ease;
}

.hx-mos--shiny .hx-mos__cell:hover .hx-mos__icon {
  background: var(--accent);
}

.hx-dims__body { max-width: none; }

.hx-vars { margin-bottom: var(--s-8); }

/* The flip. Perspective on the word, preserve-3d on the letters; each
   letter's delay rides in from the markup (--fd). The title's colour
   arrives on a delay sized to the flip, so the blue lands as the letters
   settle. */
.hx-def__flip { display: inline-block; perspective: 600px; }

.hx-def__fch,
.hx-def__fsp {
  display: inline-block;
}

.hx-def__title { transition: color 0.3s ease; }

.hx-def__item:hover .hx-def__title {
  color: var(--accent);
  transition-delay: 0.35s;
}

@media (prefers-reduced-motion: no-preference) {
  .hx-def__fch { transform-style: preserve-3d; }

  .hx-def__item:hover .hx-def__fch {
    animation: hx-def-flip 0.55s ease both;
    animation-delay: var(--fd, 0s);
  }
}

@keyframes hx-def-flip {
  from { transform: rotateX(0); }
  to { transform: rotateX(360deg); }
}

/* --- 7 · The site footer (client round, after a supplied reference) -------
   Brand and byline left, three link groups across the right, a hairline,
   then the legal line with the tagline opposite. Rides the black footer
   band above (white links, white focus rings, low-alpha hairlines); the
   blue is #4D9AFF, audited at 7.38:1 on the black in tools/contrast.mjs.
   The reference includes a "Legal" link; it ships only once it has a real
   destination. */

.hx-footer__top { padding-bottom: var(--s-16); }

/* Brand on the left, the three groups on the RIGHT (the reference's
   arrangement), with the groups' label row pushed down so the wordmark
   rides ABOVE it — the labels align with the byline, as in the
   reference. */
@media (min-width: 56em) {
  .hx-footer__top {
    display: grid;
    grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
    gap: var(--s-16);
  }

  .hx-footer__brand { margin-bottom: 0; }

  .hx-footer__groups { padding-top: 3.1rem; max-width: none; }
}

.hx-footer__brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-3);
  margin-bottom: var(--s-12);
}

.hx-footer__mark {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-weight: var(--weight-semibold);
  letter-spacing: -0.02em;
  font-size: var(--text-h3);
  text-decoration: none;
}

/* Sized to the wordmark beside it. The mark's own aspect (taller than
   wide) comes from its viewBox, so only the height is set. */
.hx-footer__glyph { display: block; height: 30px; }

.hx-footer__glyph svg { display: block; height: 100%; width: auto; }

.hx-footer .hx-footer__byline {
  font-size: var(--text-small);
  color: #4d9aff;
  text-decoration: none;
}


.hx-footer__groups { display: grid; gap: var(--s-12); }

@media (min-width: 40em) {
  .hx-footer__groups {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s-16);
    max-width: 56rem;
  }
}

.hx-footer__glabel {
  font-size: var(--text-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #4d9aff;
  font-weight: var(--weight-medium);
  margin: 0 0 var(--s-4);
}

.hx-footer__group ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-3);
}

.hx-footer__group a {
  font-size: var(--text-small);
  text-decoration: none;
  transition: color var(--fast) var(--ease-out);
}

.hx-footer__legal {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-6);
  flex-wrap: wrap;
  border-top: 1px solid rgb(255 255 255 / 0.16);
  padding-top: var(--s-6);
  font-size: var(--text-small);
}

/* --- 8 · The mark in the nav (client round) -------------------------------
   The glyph sits beside the wordmark in the top bar, at the wordmark's own
   size. It inherits `currentColor`, so it is white over the dark hero and
   ink on the 404's light ground without a second rule — the same reason
   the footer's mark needed none. */
.p-nav__mark {
  display: inline-flex;
  align-items: center;
  gap: 9px;
}

.hx-nav__glyph { display: block; height: 1.75em; }

.hx-nav__glyph svg { display: block; height: 100%; width: auto; }

/* --- 9 · The bar stays on top, and frosts on scroll (client rounds) ------
   The nav was absolute on the hero and scrolled away with it. It is FIXED
   now, holding the top of the viewport for the whole page: transparent
   over the hero, frosted glass once past it.

   THE GLASS IS THE BASE STATE AND THE TRANSPARENCY IS THE ENHANCEMENT,
   which is the opposite of how it reads on screen and is deliberate. The
   wordmark is white. A transparent bar over this page's light body — its
   white cards included — would erase it. So the stylesheet starts at the
   legible state and `nav-scroll.js` takes it away only while the dark hero
   is behind the bar. Scripts blocked, or `js` withdrawn: glass throughout,
   always readable.

   THE GLASS IS DARK for the same reason, measured rather than guessed.
   Black at 0.62 under blur composites to #666 over pure white, where white
   text measures 5.74:1; without `backdrop-filter` it firms to 0.72 for
   8.45:1, since the blur is doing none of the work there. Every other
   ground on the page is darker still.

   The transparent state is gated twice over: on `html.js`, so a reader
   without the script never gets it, and away from `[data-page='404']`,
   whose page is light at the top and would lose the mark entirely. */
.p-nav,
.p-nav.is-stuck {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 60;
  background: rgb(0 0 0 / 0.72);
  border-bottom: 1px solid rgb(255 255 255 / 0.12);
  backdrop-filter: none;
  transition: background-color 0.28s ease, border-color 0.28s ease;
}

/* The frost. `saturate` keeps colour moving under the panel from going
   flat and grey, which is most of what makes glass read as glass. */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .p-nav,
  .p-nav.is-stuck {
    background: rgb(0 0 0 / 0.62);
    -webkit-backdrop-filter: blur(18px) saturate(150%);
    backdrop-filter: blur(18px) saturate(150%);
  }
}

/* At the top of the landing page the hero's own ground is behind the bar,
   so the bar gets out of its way entirely. */
:root.js:not([data-page='404']) .p-nav.is-top {
  background: transparent;
  border-bottom-color: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

@media (prefers-reduced-motion: reduce) {
  .p-nav,
  .p-nav.is-stuck { transition: none; }
}

/* Anchored jumps must not land underneath the bar. */
html { scroll-padding-top: 80px; }

/* The 404's copy starts near the top of a short page, so it needs the room
   the bar now occupies. The long page's hero centres its copy in the
   viewport and never collided with it. */
[data-page='404'] #main { padding-top: 64px; }

/* ==========================================================================
   10 · THE MOBILE PASS (client round)
   ---------------------------------------------------------------------------
   Six fixes at 375px, and one of them is the type scale itself.
   ======================================================================= */

/* --- 1 · The scale, retuned at the tokens ---------------------------------
   The audit at 375px found ten distinct sizes between 12 and 39px, with
   every section heading at 32px on a 327px column — the headline size of a
   desktop, on a phone. The clamps were the cause: each one hits its own
   floor at this width, so the "responsive" scale stops being responsive
   exactly where it matters most.

   Retuning the TOKENS rather than the twenty-odd selectors that read them
   keeps one scale in one place: headings step down together, body steps
   down with them, and nothing drifts out of proportion with its neighbours.
   Small and label hold at 14 and 12, which are already right.

   `tools/check-type-scale.mjs` measures what the browser actually computed
   and holds it against the ladder below, so the next clamp written against
   a desktop reference cannot quietly land a ninth size on a phone. */
@media (max-width: 47.99em) {
  :root {
    --text-h2: 1.625rem;      /* 32 → 26 */
    --text-h3: 1.25rem;       /* 20, pinned off its clamp */
    --text-lede: 1.0625rem;   /* 18 → 17 */
    --text-body: 1rem;        /* 17 → 16 */
  }

  /* --- The four sizes that do not read a token ---------------------------
     Each of these carries its own clamp, written months apart against its
     own desktop reference, and each floor landed on a different number.
     At 375px they rendered 24, 21.9, 17.5 and 16.27 — four sizes where the
     scale has two. They are pinned to ladder steps here rather than having
     their clamps rewritten, so the desktop behaviour every one of them was
     tuned for is untouched. */

  /* Down to body at the client's direction: on a phone this paragraph sets
     the same size as the close-statement it argues alongside, so the two
     read as one voice rather than as a headline and its footnote.

     No contrast consequence, and it is worth saying why rather than
     assuming it. Below 18.66px a bold paragraph stops counting as large
     text and its resting tone would owe 4.5:1 instead of 3.0 — but the one
     wipe on this page is the DARK variant, which overrides the light
     resting tone entirely with #1F1E1E, a tone the audit already carries as
     a MOTION state rather than as text. It exists only inside the wipe's
     own `@supports` block, so scripts-blocked, reduced-motion and any
     browser without scroll-driven animation all read this paragraph fully
     lit: white on black, 21:1, at any size. */
  .hx-cycle-lede { font-size: var(--text-body); }

  /* The turn is the same voice at the same weight, so it takes the same
     step. Its own clamp floor was two thirds of a pixel off it — close
     enough to read as an accident, which is exactly what it was. The solo
     variant is named too: it carries a second clamp at higher specificity,
     and that is the one the phone was actually rendering. */
  .p-turn__line,
  .hx-turn--solo .p-turn__line--shimmer { font-size: 1.375rem; }

  /* Both of these were sitting between body and lede, close enough to each
     to read as neither. The second close-statement is a paragraph; the
     hero's opening line is a lede. Each joins the one it belongs to. */
  .hx-statement--second { font-size: var(--text-body); }
  .hx-hero15__lede { font-size: var(--text-lede); }
}

/* --- 2 · The cycle keeps its line, turned on its side ---------------------
   The track is horizontal and its dots sit over the words, so when the
   phases stack it pointed at nothing and was hidden — which left the band
   with no line at all. It comes back as a vertical RAIL on the left,
   running top to bottom, with the same pulse travelling down it.

   The dots move onto the phases themselves rather than staying in the
   track: the phases have unequal heights, so only a dot that belongs to a
   phase can line up with it. */
@media (max-width: 47.99em) {
  .hx-loop {
    display: grid;
    grid-template-columns: 12px minmax(0, 1fr);
    gap: var(--s-4);
    padding-top: 0;
  }

  .hx-loop__track {
    display: block;
    position: relative;
    height: auto;
    width: 12px;
    background: none;
  }

  /* The rail itself, fading out before the end exactly as the horizontal
     line does — the cycle runs one way and there is nothing at the end to
     hold, which is the whole point of the drawing.

     It starts at the CENTRE of the first dot, not at its top edge: the dot
     is opaque and covers the join, so the line reads as running through the
     dots rather than as starting just above one. */
  .hx-loop__track::before {
    content: '';
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: 13px;
    bottom: 0;
    width: 2px;
    background: linear-gradient(
      180deg,
      rgb(255 255 255 / 0.4) 0%,
      rgb(255 255 255 / 0.4) 62%,
      rgb(255 255 255 / 0.12) 82%,
      transparent 96%
    );
  }

  /* The track's own nodes cannot align with stacked phases of unequal
     height; the phases carry their own. */
  .hx-loop__node { display: none; }

  .hx-loop__phases { margin-top: 0; gap: var(--s-6); }

  .hx-loop__phase { position: relative; }

  /* CENTRED ON THE RAIL, and derived rather than eyeballed. The phase's
     left edge sits one track (12px) plus one grid gap from the loop, and
     the rail runs down the middle of that track at 6px. Pulling the dot
     back by exactly `12px + gap` puts its own left edge at 0 and its
     12px-wide centre at 6px — the same 6px. Written as the sum of the two
     values it depends on, so changing the gap cannot slide the dots off
     the line the way a hand-counted offset did. */
  .hx-loop__phase::before {
    content: '';
    position: absolute;
    left: calc(-12px - var(--s-4));
    top: 7px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #000000;
    border: 1.5px solid rgb(255 255 255 / 0.8);
  }

  /* The pulse runs down the rail instead of across the track. */
  .hx-loop__pulse {
    top: 0;
    left: 50%;
    width: 12px;
    height: 160px;
    margin-left: -6px;
    border-radius: 6px;
    background: linear-gradient(
      180deg,
      transparent 0%,
      rgb(174 226 255 / 0.35) 35%,
      #ffffff 70%,
      transparent 100%
    );
  }

  .js .hx-loop__pulse { animation-name: hx-loop-run-y; }
}

/* THE BRIGHT HEAD, NOT THE BOX, IS WHAT A READER SEES TRAVELLING. The pulse
   is a 160px gradient whose white peak sits at 70% of its own height — the
   tail trails ABOVE it, which is what makes a downward run read as a run
   rather than a sliding bar. That puts the head 112px below the element's
   top edge, so starting the box just above the rail started the visible
   head level with the SECOND dot: the first phase never got lit.

   The start offset is therefore in px against the pulse's own height, not a
   percentage of the track: -130px puts the head 18px above the rail, clear
   of the first dot at 13px. It reaches full opacity within the first few
   percent so it is already lit when it passes that dot, rather than fading
   in on top of it. The end stays a percentage — that one is about the
   track, which is the thing it has to clear. */
@keyframes hx-loop-run-y {
  0% { top: -130px; opacity: 0; }
  4% { opacity: 1; }
  70% { opacity: 1; filter: blur(4px); }
  100% { top: 100%; opacity: 0; filter: blur(14px); }
}

/* --- 3 · One message position on a narrow screen --------------------------
   On the wide layout the two message boxes sit at different heights, high
   for the over miss and low for the under, because each one belongs beside
   its bar. Stacked on a phone that becomes a box that jumps up and down as
   the chart cycles, which reads as a glitch rather than as a pairing. Here
   they share ONE grid cell: same place every time, only the message
   changes. The card takes the taller of the two, so nothing reflows.

   GATED ON `--run`, and that gate is the whole safety of it. One cell is
   one message only while the crossfade is lighting one box at a time. The
   class is added by `margin-cycle.js`, which never runs with scripts
   blocked and stands down under reduced motion — the two conditions where
   BOTH boxes are fully lit. Ungated, the second message would paint over
   the first while the bar behind them held a single state, and a reader
   would get the over-produced bar under the words "Under-produced".
   `check-a11y.mjs` measures this pair at 375px in both static states. */
@media (max-width: 47.99em) {
  .hx-mstack--run .hx-mstack__msgs { display: grid; }

  .hx-mstack--run .hx-mstack__msgs .hx-mstack__why {
    grid-area: 1 / 1;
    margin-top: 0;
  }
}

/* --- 4 · The dimension mosaic, two up ------------------------------------
   One card per row made each cell enormous, and the decorative ghost and
   filler cells kept their rows on top of that. Two columns, the decoration
   stood down, and the label at body size rather than lede. */
@media (max-width: 47.99em) {
  .hx-mos__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-areas: none;
    grid-template-rows: auto;
    /* The wide mosaic's row floor was still in force underneath, holding
       every cell at 140px whatever it contained. A card here is an icon
       and a word; the row should be the height of those two. */
    grid-auto-rows: auto;
  }

  .hx-mos__cell--1,
  .hx-mos__cell--2,
  .hx-mos__cell--3,
  .hx-mos__cell--4,
  .hx-mos__cell--5,
  .hx-mos__cell--6,
  .hx-mos__cell--7 { grid-area: auto; }

  /* Seven cards do not divide into two columns, and the odd one out left a
     half-row of bare grid ground reading as a card that failed to load.
     The last one takes the full width instead: the row is complete, and
     nothing on screen looks like a hole. */
  .hx-mos__cell--7 { grid-column: 1 / -1; }

  /* Empty cells that exist only to shape the wide mosaic. */
  .hx-mos__fill,
  .hx-mos__ghost { display: none; }

  /* `space-between` is what put the label at the bottom of a tall box. With
     the row floor gone there is no space left to distribute, so the two
     pieces sit together and the gap is the only thing separating them. */
  .hx-mos__inner {
    justify-content: flex-start;
    gap: var(--s-3);
    padding: var(--s-3);
  }

  .hx-mos__label { font-size: var(--text-body); }
}

/* --- 5 · The footer's groups, two up -------------------------------------
   Three groups in one column ran the footer long. Two per row, and the
   brand block loses the gap it was carrying: the wordmark's line box
   already holds the space, so the byline was sitting a full step further
   down than it looked in the markup. */
@media (max-width: 55.99em) {
  .hx-footer__groups {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-8) var(--s-6);
  }

  .hx-footer__brand {
    gap: 2px;
    margin-bottom: var(--s-8);
  }

  .hx-footer__mark { line-height: 1.1; }
}

/* ==========================================================================
   11 · THE SECOND MOBILE ROUND (client round)
   ---------------------------------------------------------------------------
   Two of these are colour and apply at every width; the rest are spacing.
   ======================================================================= */

/* --- 1 · The eyebrows take the accent ------------------------------------
   Four small-caps mono labels are promoted from tertiary ink to the accent
   at the client's direction: the chart's title, the profile terms, and the
   measures header. They are the labels a reader uses to navigate the page,
   and the footer's link headings already wear a blue for the same reason.

   THE ACCENT'S BUDGET GROWS BY THIS, which the type system is explicit
   about ("one accent per page: primary CTA, one data highlight, link"). It
   is a deliberate widening, not an oversight: the accent now also marks the
   label layer, and it marks ALL of it rather than some of it, which is what
   keeps it a system. Every ground these sit on is audited — #263BAA reads
   8.37 on canvas, 9.18 on surface, 7.53 on sunken, against 4.5 for text
   this size. */
.hx-mstack__label,
.hx-defs__term,
.hx-watched__label {
  color: var(--accent);
}

/* The scramble's in-flight tone follows the resting tone it returns to. Left
   grey it would flick label → grey → blue on every pass, which reads as the
   colour failing rather than as the letters settling. */
.hx-defs__term .is-scrambling { color: var(--accent-quiet); }

/* --- 2 · The footer's two closing lines ----------------------------------
   The legal row is one line on a wide screen and two on a phone, and it
   carried a single 24px gap doing both jobs. Split them: the horizontal gap
   between the two lines when they share a row is unchanged, and the gap
   they take when they stack drops to 8px, so the copyright and the line
   under it read as a pair rather than as two separate closings. */
.hx-footer__legal {
  column-gap: var(--s-6);
  row-gap: var(--s-2);
}

/* ==========================================================================
   12 · FOCUS REVEAL
   ---------------------------------------------------------------------------
   The character states for `focus-reveal.js`. Nothing here applies until the
   script has wrapped a heading and added `fx-split`, so a heading that is
   never wrapped — scripts blocked, reduced motion, no IntersectionObserver —
   is styled by none of it.

   THE HIDDEN STATE LIVES ON `fx-split`, NOT ON `.fx-char`. Both classes are
   written by the same script in the same frame, which sounds like the same
   thing and is not: keying the hidden state to the wrapper means the spans
   cannot exist in a hidden state at any moment the wrapper is absent, so
   there is no ordering in which a failed or interrupted run leaves a
   heading invisible. The worst case is text that never animates.
   ======================================================================= */

/* Words stay whole so a line breaks between words, never inside one — the
   detail that separates a split heading from a shredded one. */
.fx-word {
  display: inline-block;
  white-space: nowrap;
}

.fx-char {
  display: inline-block;
  /* The source's ease-out cubic and its 300ms. `filter` is listed last
     because it is the expensive one and the order is what a reader of this
     rule will assume it costs. */
  transition:
    opacity 300ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--fx-delay, 0ms),
    transform 300ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--fx-delay, 0ms),
    filter 300ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--fx-delay, 0ms);
}

.fx-split .fx-char {
  opacity: 0;
  transform: scale(1.45);
  filter: blur(20px);
  will-change: opacity, transform, filter;
}

.fx-split.is-focused .fx-char {
  opacity: 1;
  transform: none;
  filter: blur(0);
}

/* Belt and braces. The script does not wrap anything under reduced motion,
   so these rules should never be reachable there; if some future path
   reaches them anyway, the heading is still readable and still. */
@media (prefers-reduced-motion: reduce) {
  .fx-char {
    transition: none;
  }

  .fx-split .fx-char {
    opacity: 1;
    transform: none;
    filter: none;
    will-change: auto;
  }
}

/* ==========================================================================
   13 · TWO MORE ENTRANCES
   ---------------------------------------------------------------------------
   BOTH OF THESE HIDE A CHILD OF A REVEAL BLOCK, and that is the thing to be
   careful about rather than the animation. The reveal system carries real
   guarantees — the `js` flag is withdrawn if its script never arrives, a
   missing IntersectionObserver reveals everything, and a load-time net
   catches whatever the observer never fired for. A child that hides itself
   inherits every one of those guarantees for exactly as long as its hidden
   state is keyed to `.js` AND to the same `is-revealed` class, and none of
   them the moment it is keyed to anything else.

   So both rules below are written the same way and only that way: hidden
   under `.js .p-reveal`, resting under `.js .p-reveal.is-revealed`. There is
   no state either of them can reach that the reveal block cannot get out of.
   `check-a11y.mjs` reads them with scripts blocked and under reduced motion
   and requires full opacity, no filter and no transform.
   ======================================================================= */

/* --- 1 · The two eyebrows come out of blur -------------------------------
   Per-character would be wrong for these: both are short uppercase mono
   labels with wide tracking, and splitting letters that are already held
   apart reads as the tracking breaking rather than as an effect. The label
   resolves as ONE object instead — the same blur and the same curve as the
   headings, so it belongs to the same family without competing with them.

   Slower than the headings at 520ms, and that is deliberate. A small quiet
   label sharpening quickly reads as a flicker; the same move drawn out
   reads as focus being found. */
.js .p-reveal [data-blur-in],
.js .reveal [data-blur-in] {
  opacity: 0;
  filter: blur(8px);
  transform: translate3d(0, 6px, 0);
  /* One delay expression for every blur-in on the page: a fixed beat after
     its block arrives, plus `--i` steps for anything that is one of a set.
     A lone element leaves `--i` unset and gets the beat only, so the same
     rule serves both without a second selector. `--fx-step` is the knob a
     component turns when its own set wants a different rhythm. */
  transition:
    opacity 520ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--fx-in-delay),
    filter 520ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--fx-in-delay),
    transform 520ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--fx-in-delay);
  --fx-in-delay: calc(90ms + var(--i, 0) * var(--fx-step, 70ms));
  will-change: opacity, filter, transform;
}

.js .p-reveal.is-revealed [data-blur-in],
.js .reveal.is-revealed [data-blur-in] {
  opacity: 1;
  filter: blur(0);
  transform: none;
  will-change: auto;
}

/* --- 2 · The six defensible points fade up -------------------------------
   14px of travel, not more: these are text blocks either side of a diagram,
   and a long rise on a block that tall turns into the page moving rather
   than the content arriving.

   The stagger reads `--i`, which the component writes as the point's ROW
   rather than its reading order (DefensibleGrid.js). Both columns fill top
   to bottom together, alongside the diagram between them, instead of the
   right-hand side waiting for the left to finish. */
.js .p-reveal .hx-def__item,
.js .reveal .hx-def__item {
  opacity: 0;
  transform: translate3d(0, 14px, 0);
  transition:
    opacity 560ms cubic-bezier(0.215, 0.61, 0.355, 1),
    transform 560ms cubic-bezier(0.215, 0.61, 0.355, 1);
  transition-delay: calc(var(--i, 0) * 110ms);
  will-change: opacity, transform;
}

.js .p-reveal.is-revealed .hx-def__item,
.js .reveal.is-revealed .hx-def__item {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* Motion off: both arrive already arrived. Neither is information a reader
   can afford to wait for, and neither effect means anything without the
   movement that IS the effect. */
@media (prefers-reduced-motion: reduce) {
  .js .p-reveal [data-blur-in],
  .js .reveal [data-blur-in],
  .js .p-reveal .hx-def__item,
  .js .reveal .hx-def__item {
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
    will-change: auto;
  }
}

/* ==========================================================================
   14 · THE SAME ENTRANCE, EVERYWHERE IT WAS ASKED FOR
   ---------------------------------------------------------------------------
   The client round extends the blur reveal across most of the page's content
   and adds a plain fade to one grid. The temptation with a list that long is
   a rule per component; what is here instead is the two markers from §13 put
   on more elements, plus the one case §13 could not express.

   That case is a target that IS its own reveal element rather than something
   inside one — the seven problem cards, where the card and the revealing
   wrapper are the same `<li>`. §13's selector has a descendant combinator,
   so it cannot see them. The self selector below is the same rule keyed to
   the same class, and it adds ONLY the blur: the opacity and the travel are
   already the reveal system's job on that element, and duplicating them
   would run two transitions against each other on one property.
   ======================================================================= */

.js .p-reveal[data-blur-in],
.js .reveal[data-blur-in] {
  filter: blur(8px);
  /* Shares the reveal's own delay so the blur and the travel start together
     rather than the blur trailing its element. */
  transition-property: opacity, transform, filter;
  transition-duration: var(--slow), var(--slow), 520ms;
  transition-timing-function: var(--ease-out), var(--ease-out), cubic-bezier(0.215, 0.61, 0.355, 1);
}

.js .p-reveal.is-revealed[data-blur-in],
.js .reveal.is-revealed[data-blur-in] {
  filter: blur(0);
}

/* --- The plain fade ------------------------------------------------------
   The six watched measures are small boxes on one row, and blur on a row of
   boxes that size reads as a rendering fault rather than as an entrance —
   there is not enough inside one to be seen resolving. They rise and fade
   instead, on the same curve, so the section still arrives as a piece. */
.js .p-reveal [data-fade-in],
.js .reveal [data-fade-in] {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition:
    opacity 460ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--fx-in-delay),
    transform 460ms cubic-bezier(0.215, 0.61, 0.355, 1) var(--fx-in-delay);
  --fx-in-delay: calc(90ms + var(--i, 0) * var(--fx-step, 70ms));
  will-change: opacity, transform;
}

.js .p-reveal.is-revealed [data-fade-in],
.js .reveal.is-revealed [data-fade-in] {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* --- Rhythms, per set ----------------------------------------------------
   Each of these is one number chosen against the SIZE of its set, because a
   step that reads as a wave across six elements reads as a queue across
   nine. The rule is the last element's delay: past about half a second the
   stagger stops being a wave and becomes a wait. */

/* Nine (a label and its eight variables): 45ms, last at 450ms. */
.hx-vars { --fx-step: 45ms; }

/* Six process rows: 70ms, last at 440ms. */
.hx-tl { --fx-step: 70ms; }

/* Six watched boxes: 60ms, last at 390ms — quicker because they are small
   and side by side, where a slow queue is very visible. */
.hx-watched12 { --fx-step: 60ms; }

/* The chart and its callout are a pair, not a set. One clear beat between
   them: the chart is what the callout is about, so it goes first. */
.hx-mstack { --fx-step: 180ms; }

/* Motion off: everything above rests where it belongs, already arrived. */
@media (prefers-reduced-motion: reduce) {
  .js .p-reveal[data-blur-in],
  .js .reveal[data-blur-in],
  .js .p-reveal [data-fade-in],
  .js .reveal [data-fade-in] {
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
    will-change: auto;
  }
}

/* ==========================================================================
   15 · THE QUESTIONS
   ---------------------------------------------------------------------------
   A disclosure list built on native `<details>`. The component explains why
   it is native rather than scripted; this file's job is to make it look like
   the rest of the page and to keep the parts a browser gives away for free.

   ONE COLUMN, NOT TWO. The first build put these in two columns, which is
   right for five fixed blocks and wrong the moment they collapse: opening an
   item in a two-column grid grows its row, which moves the unrelated item
   beside it. A reader who clicks one question should not see a different one
   jump.

   TYPE MATCHES THE DEFENSIBLE CARDS, at the client's direction: the question
   takes `--text-body` at the same weight as `.hx-def__title`, the answer
   takes `--text-small` like `.hx-def__body`. Both are tokens, and the tokens
   are already retuned for phone width, so one pair of declarations covers
   both screens and cannot drift from the cards they are matched to.
   ======================================================================= */

/* THE BLOCK IS CENTRED, THE TEXT INSIDE IT IS NOT. The first build capped
   the heading and the list separately and left both against the container's
   left edge, which put a 400px gutter down the right of the section at
   1440 and read as the whole thing having slipped sideways.

   Centring the WRAPPER fixes it once: the heading and the list share one
   box, so they cannot drift out of line with each other, and the box sits
   in the middle of the page. The text stays left-aligned, because a
   question is read from its first word and centred questions of unequal
   length give the eye no edge to return to.

   44rem is the width the process timeline already occupies once centred, so
   the page's two centred list sections are the same column rather than two
   arbitrary measures. */
.hx-faq {
  max-width: 44rem;
  margin-inline: auto;
}

.hx-faq__heading {
  max-width: 20ch;
  margin-bottom: var(--s-8);
}

.hx-faq__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* A rule under every row, including the last: the list reads as a closed set
   rather than as something that ran out. */
.hx-faq__row { border-bottom: 1px solid var(--hairline); }

/* --- The control ---------------------------------------------------------
   `summary` is a button that a browser already made focusable, keyboard
   operable and announced. The only things done to it here are removing the
   default triangle and laying out the row. */
.hx-faq__summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-6);
  padding: var(--s-4) 0;
  cursor: pointer;
  list-style: none;
}

/* Two spellings of the same thing: WebKit needs its pseudo-element, everyone
   else honours `list-style`. Missing either one leaves a stray triangle. */
.hx-faq__summary::-webkit-details-marker { display: none; }
.hx-faq__summary::marker { content: ''; }

.hx-faq__summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

.hx-faq__q {
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: var(--weight-semibold, 600);
  line-height: 1.4;
  letter-spacing: -0.005em;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
}

@media (hover: hover) {
  .hx-faq__summary:hover .hx-faq__q { color: var(--accent); }
}

.hx-faq__q,
.hx-faq__sign { transition: color 200ms var(--ease-out), transform 240ms var(--ease-out); }

/* --- The sign ------------------------------------------------------------
   A plus drawn from two bars, rotated 45 degrees when the row opens so it
   becomes a cross. One rotation, two states, and no second glyph to keep in
   step with the first. */
.hx-faq__sign {
  position: relative;
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  align-self: center;
  color: var(--ink-tertiary);
}

.hx-faq__sign::before,
.hx-faq__sign::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1.5px;
  background: currentColor;
  transform: translateY(-50%);
}

.hx-faq__sign::after { transform: translateY(-50%) rotate(90deg); }

.hx-faq__item[open] .hx-faq__sign { transform: rotate(45deg); color: var(--accent); }

@media (hover: hover) {
  .hx-faq__summary:hover .hx-faq__sign { color: var(--accent); }
}

/* --- The answer ---------------------------------------------------------- */
.hx-faq__a {
  margin: 0;
  padding-bottom: var(--s-6);
  font-size: var(--text-small);
  line-height: 1.55;
  color: var(--ink-secondary);
}

/* --- Opening, where the browser allows it --------------------------------
   `<details>` has no animatable height: closed, its content is not laid out
   at all, so there is nothing to transition from. `::details-content` is the
   part that changes that, and it is recent enough that most browsers reading
   this page will not have it.

   So it is behind `@supports`, and the fallback is the browser's own instant
   toggle — which is a complete, correct disclosure and not a degraded one.
   `interpolate-size` is scoped to this list rather than set on the root,
   because it changes how `auto` animates for everything it applies to and
   nothing else on this page asked for that. */
@supports selector(::details-content) {
  .hx-faq__list { interpolate-size: allow-keywords; }

  .hx-faq__item::details-content {
    block-size: 0;
    overflow: hidden;
    transition:
      block-size 300ms var(--ease-out),
      content-visibility 300ms allow-discrete;
  }

  .hx-faq__item[open]::details-content { block-size: auto; }
}

/* Motion off: the row opens and closes, it just does not travel to get
   there. The disclosure is information, the slide is decoration. */
@media (prefers-reduced-motion: reduce) {
  .hx-faq__q,
  .hx-faq__sign { transition: none; }

  @supports selector(::details-content) {
    .hx-faq__item::details-content { transition: none; }
  }
}
