@layer components {
  /* ==========================================================================
     NAVIGATION — the chart's top neatline.
     Solid, thin, one hairline border, graduation ticks. No backdrop-filter,
     no shimmer keyframe, no breathing CTA: those are the anti-reference.
     ======================================================================== */
  .fh-nav {
    position: sticky;
    inset-block-start: 0;
    z-index: var(--l-z-nav);
    background-color: var(--l-ground);
    border-block-end: var(--l-hairline) solid var(--l-neatline);
  }

  /* 2rem, not 3rem. The brand/links/actions separators were spending 96px on
     two group breaks while the links themselves ran at 16px -- the bar's own
     structure was better separated than the items inside it, which is what made
     the row read as clumped. Trimming 32px here funds the links gap below and
     still leaves the group break twice the item gap, so the three groups stay
     legible as groups.

     2.5rem where there is room, 2rem in the tight band below -- 2.5rem overflows
     the document by 3px at exactly 1200px, the first width the full bar appears
     at, so the group break is the thing that yields there rather than the item
     rhythm. Ratio of break to item gap stays ~1.6x in both bands. */
  .fh-nav__inner {
    display: flex;
    align-items: center;
    gap: 2.5rem;
    min-height: 4rem;
  }

  .fh-nav__brand {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    text-decoration: none;
  }

  .fh-nav__brand img {
    height: 34px;
    width: auto;
  }

  /* Same magnifier as the hero field's prefix, at the same stroke weight, so the
     nav and the search control read as one icon system. */
  /* Tight to the word: the gap between links is 1rem, so the icon has to sit
     well inside that to read as part of "Search" rather than as its own item. */
  /* The glass and the caret are held to the SAME trailing footprint
     (--l-nav-mark), so a marked link pushes its neighbour by one distance
     rather than two. The glass carries the remainder as size, the caret as
     gap -- see the token's note. The svg's viewBox is tightened to its own ink
     in the markup, so the rendered glyph loses far less than the box does. */
  .fh-nav__link.fh-nav__link--icon {
    display: flex;
    align-items: center;
    gap: calc(var(--l-nav-mark) - var(--l-nav-mark-glass));
  }

  .fh-nav__glass {
    flex: 0 0 auto;
    margin-block-start: 0.0625rem;
  }

  /* 1.5rem. At 1rem a 16px semibold bar reads as one clumped run, and the
     trailing marks made some pairs look tighter than others on top of that.
     Funded by the 32px reclaimed from .fh-nav__inner above, so the bar does not
     get wider. */
  .fh-nav__links {
    display: none;
    align-items: center;
    gap: 1.5rem;
    margin: 0;
    margin-inline-end: auto;
  }

  .fh-nav__link {
    position: relative;
    display: block;
    padding-block: 1.25rem;
    color: var(--l-sounding);
    /* The incumbent bar inherited 1rem; 0.8125rem read as a step down from it. */
    font-size: var(--l-fs-body);
    font-weight: var(--l-fw-semibold);
    letter-spacing: 0.01em;
    text-decoration: none;
    white-space: nowrap;
  }

  .fh-nav__link:hover {
    color: var(--l-ink);
  }

  /* Active state is a graduation tick, not a colour swap — legible without
     relying on colour alone. */
  .fh-nav__link[aria-current="page"] {
    color: var(--l-gold);
  }

  .fh-nav__link[aria-current="page"]::after {
    content: "";
    position: absolute;
    inset-block-end: -1px;
    inset-inline: 0;
    height: 2px;
    background-color: var(--l-gold);
  }

  .fh-nav__actions {
    display: none;
    align-items: center;
    gap: 0.75rem;
    margin-inline-start: auto;
  }

  .fh-nav__toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin-inline-start: auto;
    padding: 0.5rem 0.75rem;
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-sm);
    color: var(--l-ink);
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-semibold);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
  }

  .fh-nav__drawer {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding-block: 1rem 1.5rem;
    border-block-start: var(--l-hairline) solid var(--l-neatline);
  }

  .fh-nav__drawer .fh-nav__link {
    padding-block: 0.75rem;
    font-size: var(--l-fs-body);
  }

  .fh-nav__drawer-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-block-start: 1rem;
  }

  /* The full bar needs ~1213px: brand 78 + links 601 + actions 398, plus gutters
     and group gaps. It used to swap in at 68rem (1088px), which left a ~125px
     overflow band from roughly 1088px to 1270px -- the width where the trailing
     CTA got clipped. It now swaps at 72rem and runs tighter until 82rem, which
     brings the requirement to ~997px and clears 1152px with room. */
  @media (min-width: 75rem) {
    .fh-nav__links,
    .fh-nav__actions {
      display: flex;
    }

    .fh-nav__toggle,
    .fh-nav__drawer {
      display: none;
    }
  }

  @media (min-width: 75rem) and (max-width: 87.999rem) {
    /* 0.5rem was smaller than the marks it has to coexist with, so the label
       rhythm spread 3.25x in this band against 2.1x at full width. 0.75rem
       halves that for ~16px of bar width, well inside the headroom the note
       above records. */
    .fh-nav__inner {
      gap: var(--l-space-8);
    }

    .fh-nav__links {
      gap: 1.25rem;
    }

    .fh-nav__actions {
      gap: 0.375rem;
    }

    .fh-nav__actions .fh-btn {
      padding-inline: 0.75rem;
    }
  }


  /* The skip link is declared in fh-reset.css, but `a { color: gold }` lives in
     the base layer -- and layers beat specificity, so its dark-on-gold text was
     being repainted gold-on-gold: 1:1, invisible to the keyboard user it exists
     for. Restated here in the components layer, which outranks base. */
  a.fh-skip {
    color: var(--l-on-gold);
    text-decoration: none;
  }

  /* ==========================================================================
     BUTTONS — ruled, near-square, no gradient, no glow.
     ======================================================================== */
  .fh-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.8125rem 1.375rem;
    border: var(--l-hairline) solid transparent;
    border-radius: var(--l-radius-sm);
    /* --l-fs-chrome, not --l-fs-data: see that token's note. A button is chrome,
       so it does not grow with the content scale. */
    font-size: var(--l-fs-chrome);
    font-weight: var(--l-fw-bold);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color var(--l-dur-fast) var(--l-ease),
      border-color var(--l-dur-fast) var(--l-ease),
      color var(--l-dur-fast) var(--l-ease);
  }

  .fh-btn--primary {
    background-color: var(--l-gold);
    color: var(--l-on-gold);
  }

  .fh-btn--primary:hover {
    background-color: var(--fh-yellow-hover);
  }

  .fh-btn--ghost {
    border-color: var(--l-contour-hi);
    color: var(--l-ink);
  }

  .fh-btn--ghost:hover {
    border-color: var(--l-gold);
    color: var(--l-gold);
  }

  /* A real hit area without breaking optical alignment: the padding is pulled
     back out with a matching negative margin, so the label still starts on the
     text column's edge. */
  .fh-btn--quiet {
    padding-inline: 0.625rem;
    margin-inline-start: -0.625rem;
    color: var(--l-gold);
    text-transform: none;
    letter-spacing: 0.02em;
    text-decoration: underline;
    text-decoration-color: rgb(var(--l-gold-rgb) / 0.4);
    text-underline-offset: 0.25em;
  }

  .fh-btn--quiet:hover {
    text-decoration-color: rgb(var(--l-gold-rgb) / 1);
  }

  /* ==========================================================================
     POSITION ENTRY — the chart's coordinate field, and the page's primary
     action. On the incumbent site this input was the least visible element in
     the viewport with no focus state at all; here it is the loudest.
     ======================================================================== */
  /* The search field is the hero of this surface: an attack-surface search
     engine, so the query box outranks every other element in the viewport. */
  .fh-entry {
    position: relative;
    width: 100%;
    min-width: 0;
    max-width: min(46rem, 100%);
  }

  .fh-entry__form {
    display: flex;
    align-items: stretch;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-contour-hi);
    border-radius: var(--l-radius-sm);
    transition: border-color var(--l-dur-base) var(--l-ease);
  }

  .fh-entry__form:hover {
    border-color: var(--l-land-edge);
  }

  /* Focus-within paints the whole field, so the affordance is unmissable and
     survives the browser's own focus ring rather than replacing it. */
  .fh-entry__form:focus-within {
    border-color: var(--l-gold);
    box-shadow: 0 0 0 3px rgb(var(--l-gold-rgb) / 0.22);
  }

  .fh-entry__prefix {
    display: flex;
    align-items: center;
    padding-inline: 1rem 0;
    color: var(--l-label);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-data);
    user-select: none;
  }

  .fh-entry__input {
    flex: 1 1 auto;
    min-width: 0;
    padding: 1.3125rem 0.875rem;
    background: transparent;
    border: 0;
    color: var(--l-ink);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-lead);
    text-overflow: ellipsis;
  }

  /* The hint is quieter and smaller than a typed query: JetBrains Mono advances
     0.6em, so this 43-character placeholder needs 516px at lead size and the
     field only offers ~480px -- it was losing the domain to an ellipsis, which
     is the one part that teaches the query format. The value itself stays at
     lead size. */
  /* THE PLACEHOLDER IS PROSE, SO IT IS NEITHER MONO NOR ON THE DATA STEP.
     DESIGN.md's The Placeholder Is Prose Rule was written for this exact
     element and then only ever applied page-locally, in search/index.html --
     so home.html has been rendering the owner's placeholder in JetBrains Mono
     the whole time, which is the 'robotic' costume the rule exists to stop.
     Fixing it here retires the page-local override.

     It also BUYS headroom rather than spending it. Measured on / at 1440 with
     the real webfonts loaded (document.fonts.check confirmed -- a headless
     fallback would have lied), against the 540px field:
       mono 13px      484px   56px spare   <- before
       mono 14px      521px   19px spare   <- what a naive --l-fs-data bump
                                              would have left, and the owner's
                                              copy losing `kaspersky.com` to an
                                              ellipsis is a recorded defect
       Urbanist 15px  411px  129px spare   <- this
     Bigger type and more than twice the slack, because Urbanist is far
     narrower than a 0.6em-per-glyph mono. */
  .fh-entry__input::placeholder {
    color: var(--l-label);
    font-family: var(--l-face-display);
    font-size: var(--l-fs-small);
    /* Kept: `.fh-entry__input:focus::placeholder` sets opacity 0, and this is
       the transition that fades it. Dropping it would silently remove motion. */
    transition: opacity var(--l-dur-fast) var(--l-ease);
  }

  /* The hint clears the moment the field is focused, as it did on the incumbent
     site: once you are typing a domain, the example is in the way. */
  .fh-entry__input:focus::placeholder {
    opacity: 0;
  }

  /* The invalid state, replacing the browser's own validation bubble. */
  /* The invalid ring must replace the gold one on every side, shadow included:
     a red border sitting inside a gold glow reads as two conflicting states. */
  .fh-entry__form[data-invalid],
  .fh-entry__form[data-invalid]:hover,
  .fh-entry__form[data-invalid]:focus-within {
    border-color: var(--fh-sev-critical);
    box-shadow: 0 0 0 3px rgb(var(--l-critical-rgb) / 0.22);
  }

  .fh-entry__error {
    display: flex;
    align-items: center;
    gap: 0.4375rem;
    margin-block-start: 0.625rem;
    color: var(--l-ink);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-data);
  }

  .fh-entry__error:empty {
    display: none;
  }

  /* Shape as well as colour: critical red is 4.7:1 here, fine for a mark but
     under the label floor, so it never sets the text. */
  .fh-entry__error::before {
    content: "";
    flex: none;
    inline-size: 0.5rem;
    block-size: 0.5rem;
    background-color: var(--fh-sev-critical);
    clip-path: polygon(50% 0, 100% 100%, 0 100%);
  }


  /* ---- Suggestion listbox. Overlays rather than pushing the CTAs down, so the
          hero geometry does not move while typing. ---- */
  .fh-entry__list {
    position: absolute;
    z-index: var(--l-z-dropdown);
    inset-inline: 0;
    margin: 0.375rem 0 0;
    padding: 0;
    max-block-size: 17rem;
    overflow-y: auto;
    list-style: none;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-contour-hi);
    border-radius: var(--l-radius-sm);
  }

  .fh-entry__list[hidden] { display: none; }

  .fh-entry__option {
    padding: 0.5rem 0.875rem;
    color: var(--l-sounding);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-small);
    cursor: pointer;
  }

  .fh-entry__option + .fh-entry__option {
    border-block-start: var(--l-hairline) solid rgb(var(--l-neatline-rgb) / 0.7);
  }

  .fh-entry__option:hover,
  .fh-entry__option[aria-selected="true"] {
    background-color: var(--l-shoal);
    color: var(--l-ink);
  }

  .fh-entry__submit {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.375rem;
    padding-inline: 1.5rem;
    background-color: var(--l-gold);
    border-radius: var(--l-radius-sm);
    color: var(--l-on-gold);
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-bold);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
  }

  .fh-entry__submit:hover {
    background-color: var(--fh-yellow-hover);
  }

  /* THE HINT IS A SENTENCE, SO IT IS NOT MONO.

     The Placeholder Is Prose Rule already draws this line and names this
     element's neighbours: "the query is mono, the label is not, the hint is
     not, the error message is not, and an example query inside a chip IS."
     The hint carried `--l-face-data` anyway, so `Read more about search
     filters` — nine words of English — set in JetBrains Mono on the search
     results page, and `examples:` did the same in the home hero.

     The face moves DOWN to `.fh-entry__sample`, which is where the machine
     input actually lives (`domain:kaspersky.com`, `kaspersky.com port:80`).
     So the row keeps its mono exactly where the rule says it belongs and
     loses it everywhere it does not. */
  .fh-entry__hint {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem 0.75rem;
    margin-block-start: 0.875rem;
    font-size: var(--l-fs-data);
    color: var(--l-label);
  }

  .fh-entry__scope {
    margin-block-start: 0.625rem;
    max-width: 40rem;
    color: var(--l-label);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-normal);
  }

  .fh-entry__sample {
    /* The one mono thing in the hint row: an example query is machine input,
       the same reason `.fh-entry__input` is mono. Moved here off the hint. */
    font-family: var(--l-face-data);
    color: var(--l-gold);
    text-decoration: none;
    border-block-end: var(--l-hairline) dashed rgb(var(--l-gold-rgb) / 0.5);
  }

  /* ==========================================================================
     CHIP — the chart's stamped annotation.
     ======================================================================== */
  .fh-chip {
    display: inline-flex;
    max-width: 100%;
    align-items: center;
    gap: 0.5rem;
    padding: 0.375rem 0.6875rem;
    border: var(--l-hairline) solid rgb(var(--l-gold-rgb) / 0.4);
    border-radius: var(--l-radius-sm);
    color: var(--l-gold);
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-semibold);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
  }

  /* For an actual warning -- CISA KEV membership, not an announcement; news
     takes --plain. The violet was rejected in review: at 4.97:1 it was the
     weakest text on the block and read as decoration rather than a flag. The
     severity now rides the border, which only has a 3:1 floor to meet, and the
     label is ink at 16.4:1. Same reasoning as The Severity Ink Rule. */
  .fh-chip--caution {
    border-color: rgb(var(--l-critical-rgb) / 0.55);
    color: var(--l-ink);
  }

  .fh-chip--plain {
    border-color: var(--l-neatline);
    color: var(--l-label);
  }

  /* ==========================================================================
     LEGEND — a chart legend, which is what a capability list actually is:
     symbol, name, meaning. Numbered, ruled, no card shell, no hover lift.
     ======================================================================== */
  .fh-legend {
    display: grid;
    gap: 0;
    border-block-start: var(--l-hairline) solid var(--l-neatline);
  }

  .fh-legend__item {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.5rem 1.25rem;
    padding-block: 1.5rem;
    border-block-end: var(--l-hairline) solid var(--l-neatline);
  }

  /* Rows that carry a drawn chart symbol get a symbol gutter; rows without one
     run full width rather than reserving an empty column.

     An INDEX takes the same gutter. It is the same kind of thing -- the mark in
     a legend's left margin that identifies the row -- and it was the only one
     of the two left out, so in a one-column row `01` stacked ABOVE its own
     title and read as a chapter number rather than as marginalia. That is the
     numbered-section tic, one element away from the tracked-caps kicker this
     rule set already bans. In the gutter it is what DESIGN.md's Legend entry
     describes: a 2.25rem symbol gutter, then a bold index or title.
     The two never co-occur -- 0 of the 24 index rows carry a symbol -- so the
     gutter is never asked to hold both. */
  .fh-legend__item:has(.fh-legend__symbol),
  .fh-legend__item:has(.fh-legend__index) {
    grid-template-columns: 2.25rem minmax(0, 1fr);
  }

  .fh-legend__index {
    color: var(--l-label);
    font-size: var(--l-fs-data);
    font-weight: var(--l-fw-bold);
    letter-spacing: 0.08em;
    padding-block-start: 0.25rem;
  }

  .fh-legend__title {
    margin: 0;
    color: var(--l-ink);
    font-size: var(--l-fs-d3);
    font-weight: var(--l-fw-bold);
    letter-spacing: -0.01em;
  }

  .fh-legend__body {
    margin-block-start: 0.5rem;
    max-width: 44rem;
    color: var(--l-sounding);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-relaxed);
  }

  .fh-legend__symbol {
    grid-column: 1;
    grid-row: 1;
    align-self: start;
  }

  @media (min-width: 48rem) {
    .fh-legend--paired {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      column-gap: var(--l-space-14);
    }
  }

  /* Shape-coded symbols. Severity and kind are carried by FORM first, colour
     second, which is what WCAG 1.4.1 requires and what a chart does anyway. */
  .fh-symbol {
    width: 1.75rem;
    height: 1.75rem;
    color: var(--l-gold);
  }

  /* ==========================================================================
     DATA BLOCK — the chart's tabular matter: endpoints, responses, positions.
     ======================================================================== */
  .fh-block {
    min-width: 0;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-md);
  }

  .fh-block__head {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem 1rem;
    border-block-end: var(--l-hairline) solid var(--l-neatline);
    color: var(--l-label);
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-bold);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
  }

  .fh-block__head-end {
    margin-inline-start: auto;
    display: flex;
    align-items: center;
    gap: 0.75rem;
  }

  .fh-block__body {
    padding: 1rem;
    overflow-x: auto;
  }

  .fh-block__body--flush {
    padding: 0;
  }

  .fh-endpoint {
    display: grid;
    grid-template-columns: 3rem minmax(0, 1fr);
    gap: 0.25rem 0.75rem;
    padding: 0.75rem 1rem;
    border-block-end: var(--l-hairline) solid rgb(var(--l-neatline-rgb) / 0.5);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-data);
  }

  .fh-endpoint:last-child {
    border-block-end: 0;
  }

  .fh-endpoint__verb {
    color: var(--l-gold);
    font-weight: var(--l-fw-bold);
    letter-spacing: 0.06em;
  }

  .fh-endpoint__path {
    color: var(--l-ink);
    word-break: break-all;
  }

  .fh-endpoint__note {
    grid-column: 2;
    color: var(--l-label);
  }

  .fh-code {
    margin: 0;
    padding: 1rem;
    overflow-x: auto;
    color: var(--l-sounding);
    font-size: var(--l-fs-data);
    line-height: 1.7;
    tab-size: 2;
  }

  .fh-code b {
    color: var(--l-ink);
    font-weight: var(--l-fw-medium);
  }

  .fh-code i {
    color: var(--l-gold);
    font-style: normal;
  }

  /* Booleans and numbers. Distinct from keys (ink) and strings (gold) by weight
     rather than by a third hue. */
  .fh-code u {
    color: var(--l-sounding);
    font-weight: var(--l-fw-bold);
    text-decoration: none;
  }

  /* ==========================================================================
     TABLE — a chart's index of soundings.
     ======================================================================== */
  .fh-table {
    width: 100%;
    font-size: var(--l-fs-data);
    text-align: left;
  }

  .fh-table caption {
    padding: 0 1rem 0.75rem;
    color: var(--l-label);
    font-size: var(--l-fs-label);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
    text-align: left;
  }

  .fh-table th {
    padding: 0.625rem 1rem;
    border-block-end: var(--l-hairline) solid var(--l-neatline);
    color: var(--l-label);
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-bold);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
    white-space: nowrap;
  }

  .fh-table td {
    padding: 0.75rem 1rem;
    border-block-end: var(--l-hairline) solid rgb(var(--l-neatline-rgb) / 0.45);
    color: var(--l-sounding);
  }

  .fh-table tbody tr:last-child td {
    border-block-end: 0;
  }

  /* A ROW header is a NAME, not a column label -- and `.fh-table th` above sets
     the label register on every `th`: 11px, 0.11em tracking, uppercase, `label`
     tone. Right for `<thead>`, wrong for `<th scope="row">`.

     What it was actually doing, on two live pages:
       search/filters.html  `is_dos_defense` -> `IS_DOS_DEFENSE`, on the page that
                            documents a CASE-SENSITIVE query language, where the
                            wrong case is a wrong answer a reader will copy.
       pricing/asm.html     `Attack Surface Discovery` -> tracked caps, which The
                            Tracked Caps Rule forbids outright: the label register
                            "never sets a heading, a sentence, or a paragraph".
     An identifier is not a label either.

     `.fh-table th.fh-table__name`, NOT `.fh-table__name`. The bare class is
     (0,1,0) and loses to `.fh-table th` at (0,1,1) -- which is why the
     `color: var(--l-ink)` below had no effect and these cells measured
     `--l-label` in the browser. Same specificity trap as `.fh-sheet__title--id`;
     check the computed value, not the declaration. */
  /* `th[scope="row"]` is included because it is the same thing said in markup
     rather than in a class: `search/host.html` uses the bare form for its eight
     record keys (`Organization`, `ISP`, `ASN`...). At 16px in tracked caps
     `ORGANIZATION` ran 159px and squeezed the value column until
     `United Kingdom` wrapped. The other 137 row headers on the site already
     carry the class, so this only widens the rule to the one template that
     spells it the other way. */
  .fh-table th[scope="row"],
  .fh-table th.fh-table__name,
  .fh-table__name {
    color: var(--l-ink);
    font-size: var(--l-fs-data);
    font-weight: var(--l-fw-semibold);
    letter-spacing: normal;
    text-transform: none;
  }

  .fh-table__num {
    text-align: right;
    font-variant-numeric: tabular-nums;
  }

  /* ==========================================================================
     MATRIX — a comparison grid, not an index of soundings.

     `.fh-table` rules horizontally only, and that is right for what it was
     built for: an index where one row is one record, read left to right once
     and never compared with its neighbour. The two pricing matrices are not
     that. They are read DOWN a column as much as across a row, they run 1138px
     wide, and they are 1406px and 2089px tall. On the plain index styling a
     tick sat up to 900px from its row label with nothing between them and its
     column header long gone off the top of the screen, so the reader had to
     count headers to find out which plan a tick belonged to.

     This modifier adds the three devices a chart uses for exactly that job --
     meridians, a banded index, and a fixed margin -- and nothing else. It does
     NOT add zebra striping, which DESIGN.md rules out and which would fight
     the group bands for the same signal.
     ======================================================================== */

  /* 1. MERIDIANS. A hairline between columns at the same 45% weight the cell
        rules already use, so the grid reads as one ruled field rather than as
        rows that happen to have verticals. `+` never reaches a group row: a
        `th[colspan]` is the only cell in its row, so it spans the meridians
        instead of being cut by them, which is what makes the band read as a
        break in the grid. */
  .fh-table--matrix :is(th, td) + :is(th, td) {
    border-inline-start: var(--l-hairline) solid rgb(var(--l-neatline-rgb) / 0.45);
  }

  /* 2. THE BANDED INDEX. The incumbent set these rows in gold on a 4% gold
        wash (pricing/console.html:829 at master) and the migration dropped
        both, leaving the category rows in `label` grey at the same value as
        the data rows -- the single change that turned a 40-row table into an
        unbroken wall. The band comes back as a value step to `shoal` rather
        than a gold tint, because this system separates by substrate value and
        keeps gold for ink; the result carries more structure than the
        incumbent had, not less. Gold on shoal measures 9.07:1.

        `th[scope="colgroup"]` inherits the label register from `.fh-table th`
        -- tracked caps, 12px -- and keeps it. A category name IS chart
        marginalia, so The Tracked Caps Rule is satisfied rather than dodged. */
  .fh-table--matrix th[scope="colgroup"] {
    padding-block: 0.8125rem;
    background-color: var(--l-shoal);
    border-block: var(--l-hairline) solid var(--l-neatline);
    color: var(--l-gold);
    font-weight: var(--l-fw-bold);
  }

  /* The first band sits directly under the header rule; two hairlines 0px
     apart print as one thick line. */
  .fh-table--matrix tbody tr:first-child th[scope="colgroup"] {
    border-block-start: 0;
  }

  /* 3. TRACKING A ROW. Hover moves the row to the same `shoal` the bands use.
        DESIGN.md:497 already lists table rows among the surfaces that respond
        to hover; this is that rule finally implemented. It is an aid, not a
        state, so it is a substrate step and not a colour change. */
  @media (hover: hover) {
    .fh-table--matrix tbody tr:hover :is(th, td):not([scope="colgroup"]) {
      background-color: rgb(var(--l-neatline-rgb) / 0.22);
    }
  }

  /* The subject column needs a floor or the browser hands it to whichever
     column has the longest prose. 17.5rem is the incumbent's 280px
     (`.pc-table__feature-col`, pricing/console.html:815 at master). Below
     62rem the block is still a horizontal scroller, so the floor is what the
     reader scrolls rather than something that has to fit. */
  .fh-table--matrix th[scope="row"] {
    min-width: 17.5rem;
  }

  /* A DEFINITE END. `.fh-table` unrules its last row (fh-components.css:714)
     because an index sits inside a block whose own border closes it. At 1.84:1
     on `deep` that border is a structural hairline, not a stop line, and on a
     table 1406px and 2089px tall the reader reaches the bottom without ever
     being told they have -- "I can't tell if this is the end of the page or
     there's more" is the literal report. The matrix closes on the same
     full-strength neatline it opens on, so the grid ends on a drawn line
     rather than fading into the sheet. */
  .fh-table--matrix tbody tr:last-child :is(th, td) {
    border-block-end: var(--l-hairline) solid var(--l-neatline);
  }

  /* 4. THE FIXED MARGIN. A chart keeps its scale visible while you read the
        sheet. `.fh-block__body` is `overflow-x: auto`, which computes
        `overflow-y` to `auto` as well and so makes the block its own scroll
        container -- a `position: sticky` header inside it would pin to a box
        that never scrolls vertically and therefore never move. The matrix
        fits its container without horizontal scroll from 62rem up (measured:
        1138px table in a 1138px body at 1440), so above that width the
        container is released and the header pins to the viewport under the
        4rem nav instead. Below it the scroller comes back and the header
        scrolls away with the table, as it does today. */
  @media (min-width: 62rem) {
    .fh-block__body--matrix {
      overflow: visible;
    }

    /* `:is(th, td)` because the blank corner of a two-axis matrix is a `<td>`,
       not an empty `<th>` -- pricing/index.html:145 makes that swap on purpose,
       since an empty table header names nothing and axe flags it. Selecting
       only `th` left the corner unpainted and unpinned, so the band opened
       with a notch and the corner scrolled out from under the rest of it. */
    .fh-table--matrix thead :is(th, td) {
      position: sticky;
      inset-block-start: var(--l-nav-h);
      z-index: 1;
      background-color: var(--l-shoal);
      color: var(--l-gold);
    }
  }

  /* MARKS. The yes/no variant. The incumbent centred every plan column
     (pricing/console.html:822 at master) and dimmed the absent mark to #484f58
     (:839); the migration left both columns ragged-left in one uniform ink, so
     101 ticks and 18 dashes carried the same weight and sat nowhere near their
     header. Centring is what puts a mark under its own column label. */
  .fh-table--marks :is(thead th, td):not(:first-child) {
    text-align: center;
  }

  .fh-table--marks td {
    vertical-align: middle;
  }

  /* Shape first, colour second: a tick and a dash are already different
     silhouettes, so the colour step only reinforces what the glyph says.
     Each mark carries its own text alternative in the markup -- a bare
     `&#10003;` in a cell announces as "checkmark" or as nothing at all. */
  .fh-mark {
    font-size: 1.0625rem;
    line-height: 1;
  }

  .fh-mark--yes {
    color: var(--l-gold);
  }

  .fh-mark--no {
    color: var(--l-faint);
  }

  /* A value, not a mark. Prices, credit counts and licence terms share the
     plan columns with the ticks, and they are the rows a reader actually
     compares, so they step up to `ink` and take tabular figures to align
     $149 / $1,499 down the column.

     NOT mono, though `.fh-table__value` first shipped that way. The Mono Is
     Measurement Rule reserves the data face for machine output, and half of
     these cells are words -- "Eval only", "Internal use", "Commercial",
     "Up to 80 companies". Setting those in mono claims they are readings.
     They stay in the display face the rest of the table body already uses;
     the value step carries the emphasis on its own. */
  .fh-table--marks .fh-table__value {
    color: var(--l-ink);
    font-weight: var(--l-fw-medium);
    font-variant-numeric: tabular-nums;
  }

  /* Risk marks: shape + label, never colour alone. */
  .fh-risk {
    display: inline-flex;
    align-items: center;
    gap: 0.4375rem;
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-bold);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  .fh-risk::before {
    content: "";
    width: 0.5rem;
    height: 0.5rem;
    flex: 0 0 auto;
    background-color: currentColor;
  }

  .fh-risk--low {
    color: var(--fh-sev-low);
  }

  /* A diamond for medium, a triangle for high: the chart's obstruction marks. */
  .fh-risk--medium {
    color: var(--fh-sev-medium);
  }

  .fh-risk--medium::before {
    rotate: 45deg;
  }

  .fh-risk--high {
    color: var(--fh-sev-high);
  }

  .fh-risk--high::before {
    background-color: transparent;
    width: 0;
    height: 0;
    border-inline: 0.3125rem solid transparent;
    border-block-end: 0.5rem solid currentColor;
  }

  .fh-risk--critical {
    color: var(--fh-sev-critical);
  }

  .fh-risk--critical::before {
    border-radius: 50%;
    box-shadow: 0 0 0 2px currentColor inset;
    background-color: transparent;
  }

  /* ==========================================================================
     FIGURES — the survey record. Large tabular numerals, ruled, no counters
     animating on scroll.
     ======================================================================== */
  /* AN EMPTY CELL MUST BE INDISTINGUISHABLE FROM A FIGURE.

     This grid used to paint `--l-neatline` as its own background and let it show
     through the 1px gaps -- an elegant way to get hairlines, and correct while
     every cell is occupied. But `auto-fit` collapses empty TRACKS, not an empty
     CELL in a partially-filled last row. So whenever the item count is not a
     multiple of the resolved column count, the leftover cell had no `.fh-figure`
     to cover the neatline and rendered as a large, lighter, EMPTY BOX.

     Measured across four widths before touching it, because the first report
     looked like one page and was not -- it leaked on FIVE:
       1440 / 1024px : /integrations/, /love/, /resources/guides/  (3 items, 2 tracks)
       768px         : /pricing/asm/, /products/oem-apis/          (4 items, 3 tracks)
       390px         : none (a single track always fills)

     A rejected fix, recorded because it looks right and is not: `grid-column-end:
     -1` on the last child. It does make the last figure end on the final line,
     but the start stays auto, so auto-placement drops the item into the LAST
     column and the empty cell simply moves to the front of the row. And there is
     no general pure-CSS fix that fills a partial row, because CSS cannot know the
     resolved column count.

     So the mechanism changes instead: the container now carries the same
     `--l-deep` ground as the figures, and each figure draws its own hairline with
     an `outline`. Between two figures the two outlines meet across the 1px gap
     and read as one hairline; at the outer edge they form the container's edge.
     An unoccupied cell paints `--l-deep`, which is exactly what an occupied one
     paints, so the leftover cell is invisible at every count and every width.

     `outline`, not `box-shadow`: the Flat Sheet Rule bans box-shadow outright,
     and an outline is a line rather than a shadow. It also takes no space, which
     is what lets it bridge the gap. `overflow: hidden` clips the corner outlines
     to the container radius. */
  .fh-figures {
    display: grid;
    gap: 1px;
    background-color: var(--l-deep);
    border-radius: var(--l-radius-md);
    overflow: hidden;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
  }

  .fh-figure {
    padding: 1.5rem;
    background-color: var(--l-deep);
    outline: var(--l-hairline) solid var(--l-neatline);
  }

  .fh-figure__value {
    display: block;
    color: var(--l-gold);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-figure);
    font-weight: var(--l-fw-bold);
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
  }

  /* THE PLACEHOLDER STATE: one bullet per digit, held until the count runs.
     Applied only by fh-ui.js initFigureCount(), which has already bailed out
     under reduced motion and without IntersectionObserver — so a masked figure
     only ever exists on a page that is definitely going to animate it.

     Dimmed to the label tone rather than left gold: at full strength a row of
     bullets reads as a value in its own right, which is the opposite of what a
     placeholder should say. The colour returns with the first digit, so the
     figure brightens as it counts. `letter-spacing: normal` because the -0.02em
     tightening is tuned for digits and visibly crowds a run of bullets. */
  .fh-figure__value.is-masked {
    color: var(--l-label);
    letter-spacing: normal;
  }

  .fh-figure__label {
    display: block;
    margin-block-start: 0.625rem;
    color: var(--l-ink);
    font-size: var(--l-fs-small);
    font-weight: var(--l-fw-semibold);
  }

  /* ==========================================================================
     THE ROW VARIANT — a stat row that is a ROW, not a grid.

     `.fh-figures` is an auto-fit grid with a 13rem track minimum. In a full
     plate that resolves to one line and reads correctly: auto-fit collapses
     empty TRACKS, so three items in a 1140px container render as three cells
     sharing hairlines with nothing left over.

     In the narrow half of a `--split` it resolves to TWO columns, and three
     items then wrap to a second row with the fourth cell ruled and empty. That
     is a different failure from the one The Empty Cell Rule fixes: that rule
     makes an unoccupied cell paint what an occupied one paints, which works for
     a trailing gap on the only row and cannot fill a hole in a 2x2.

     Nor can the grid be widened out of it -- measured at 569px, "Millions"
     alone needs ~192px at --l-fs-figure, so three across does not fit either.
     At this width a stat row wants no cells at all, which is what the design
     this replaced already did.

     THREE CALL SITES, so it is here and not in three `page_css` blocks:
     `/love/`, `/integrations/`, `/resources/guides/`. Restored by owner
     instruction 2026-08-27.

     `.fh-figure__value` KEEPS ITS CLASS, which is the point of doing this as a
     modifier: initFigureCount() (fh-ui.js:448) binds to that selector, so the
     scroll count-up survives the change instead of being traded away for it.

     Values move to the PROSE face here. The Figure Rule puts a number a SYSTEM
     produced in mono; `8+`, `REST` and `Millions` are hand-written claims, and
     The Mono Is Measurement Rule then keeps them out of the machine face.
     ========================================================================= */
  .fh-figures--row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--l-space-8);
    padding-block-start: 1.5rem;
    background-color: transparent;
    border-block-start: var(--l-hairline) solid var(--l-neatline);
    border-radius: 0;
    overflow: visible;
  }

  .fh-figures--row .fh-figure {
    padding: 0;
    background-color: transparent;
    outline: 0;
  }

  .fh-figures--row .fh-figure__value {
    font-family: var(--l-face-display);
    font-size: var(--l-fs-d3);
    font-weight: var(--l-fw-black);
    line-height: var(--l-lh-snug);
    letter-spacing: normal;
  }

  .fh-figures--row .fh-figure__label {
    margin-block-start: 0.25rem;
    color: var(--l-label);
    font-weight: var(--l-fw-regular);
  }

  .fh-figure__note {
    display: block;
    margin-block-start: 0.375rem;
    color: var(--l-label);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-normal);
  }

  /* ==========================================================================
     PANEL — a bordered block of prose with a heading. The generic container,
     used where a chart would rule off a note.
     ======================================================================== */
  .fh-panel {
    min-width: 0;
    padding: 1.75rem;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-md);
  }

  .fh-panel__title {
    margin: 0;
    color: var(--l-ink);
    font-size: var(--l-fs-d3);
    font-weight: var(--l-fw-bold);
  }

  .fh-panel__body {
    margin-block-start: 0.75rem;
    color: var(--l-sounding);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-relaxed);
  }

  .fh-ticks {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.25rem;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .fh-ticks li {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--l-sounding);
    font-size: var(--l-fs-data);
  }

  .fh-ticks li::before {
    content: "";
    width: 0.4375rem;
    height: 0.4375rem;
    flex: 0 0 auto;
    border: var(--l-hairline) solid var(--l-gold);
    rotate: 45deg;
  }

  /* A sentence-length feature list is a RULED list, not a wrapping run.

     The base .fh-ticks is a flex-wrap row, which is correct for 2-4 word
     tokens ("Isolated tenants", "Sub-second responses", home.html:334) and
     wrong the moment an item is a sentence: two long items land side by side
     on one row with nothing between them and read as a single run-on line.
     Caught on /products/enterprise-asm/, where "Organization and business
     unit attribution" and "Certificate tracking and expiry monitoring"
     shared a row at desktop widths. It is a width-dependent pairing, so it
     moves with the viewport rather than showing up at one breakpoint.

     DESIGN.md's Legend entry calls a feature list "a ruled .fh-ticks list" --
     ruled is the documented intent, and this modifier is it: one item per
     row, separated by the same hairline .fh-table uses on its cells
     (fh-components.css:657), last row unruled. Hairline only -- the No
     Side-Tab Rule bans an edge accent and the Flat Sheet Rule bans a shadow,
     so the rule between rows IS the separation. */
  .fh-ticks--ruled {
    display: block;
    /* .fh-stack is a flex column with `align-items: flex-start`, so a block
       child shrink-wraps: measured 324.8px inside a 569.1px column, which
       under the base flex-wrap row was invisible and as a ruled list is not
       -- the rules stopped 244px short of the lead paragraph directly above
       them and read as arbitrary. Stretching makes every rule end on the
       lead's own right edge, which is the alignment .fh-table gets for free
       by being a table. Inert outside a flex parent. */
    align-self: stretch;
  }

  .fh-ticks--ruled li {
    align-items: start;
    padding-block: 0.5rem;
    border-block-end: var(--l-hairline) solid rgb(var(--l-neatline-rgb) / 0.45);
    line-height: var(--l-lh-normal);
  }

  /* `align-items: start` puts the diamond at the top of the text BOX, not on
     the first line, so a two-line item would hang it above the cap height.
     0.375rem drops it onto the optical centre of the first line: the line box
     is 0.8125rem x 1.5 = 1.219rem, so its centre is 0.609rem, and the glyph's
     own half-height is 0.219rem. */
  .fh-ticks--ruled li::before {
    margin-block-start: 0.375rem;
  }

  .fh-ticks--ruled li:first-child {
    padding-block-start: 0;
  }

  .fh-ticks--ruled li:first-child::before {
    margin-block-start: 0.1875rem;
  }

  .fh-ticks--ruled li:last-child {
    padding-block-end: 0;
    border-block-end: 0;
  }

  /* ==========================================================================
     NOTICES — Notices to Mariners: the running correction bulletin. This is
     the live vulnerability-check feed. It scrolls only on hover-capable
     pointers and stops dead under reduced motion.
     ======================================================================== */
  .fh-notices {
    min-width: 0;
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-md);
    background-color: var(--l-deep);
    overflow: hidden;
  }

  .fh-notices__list {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
    max-height: 22rem;
    overflow-y: auto;
  }

  .fh-notice {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.25rem 1rem;
    padding: 0.875rem 1rem;
    border-block-end: var(--l-hairline) solid rgb(var(--l-neatline-rgb) / 0.45);
  }

  .fh-notice__name {
    color: var(--l-ink);
    font-size: var(--l-fs-data);
    word-break: break-word;
  }

  .fh-notice__when {
    color: var(--l-label);
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-semibold);
    white-space: nowrap;
    text-align: right;
  }

  .fh-notice__empty {
    padding: 1.5rem 1rem;
    color: var(--l-label);
    font-size: var(--l-fs-data);
  }

  /* ==========================================================================
     ADJOINING SHEETS — the two products, presented as two sheets of one chart
     series, which is literally how a chart index shows neighbouring coverage.
     ======================================================================== */
  .fh-sheet {
    display: flex;
    flex-direction: column;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-md);
    overflow: hidden;
  }

  /* A mounted plate: the screenshot sits inset on a recessed mat with even
     breathing room, framed by its own hairline — the way a chart is pasted onto
     a mounting sheet. It is not a cropped banner bleeding to the card edge.
     Recessed by colour, not by shadow, so The Flat Sheet Rule still holds. */
  .fh-sheet__figure {
    margin: 0;
    padding: 1.25rem;
    border-block-end: var(--l-hairline) solid var(--l-neatline);
    background-color: var(--l-ground);
  }

  .fh-sheet__figure img {
    width: 100%;
    height: auto;
    aspect-ratio: 3 / 2;
    object-fit: contain;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-sm);
  }

  /* A FIGURE HOLDING A MARK, NOT A SCREENSHOT.

     The rule above is right for a product screenshot: 3/2 is the shape of the
     thing, and the inset border reads as the edge of a screen. It is wrong for
     a logo. `integrations.html` and `love.html` put wordmarks in here -- one is
     195x30 -- and `object-fit: contain` inside a forced 3/2 box left roughly
     170px of empty space under every mark, INSIDE the img's own border. The
     border made it read as a deliberate empty box rather than as slack.

     So a mark gets no box at all: it sits on the figure's own ground at a fixed
     height so the cards still align across the grid, contained so an odd aspect
     cannot crop, and centred. `height` plus `object-fit: contain` is what keeps
     a 36x48 mark and a 195x30 wordmark on the same optical line. */
  .fh-sheet__figure--mark img {
    width: 100%;
    height: 3.5rem;
    aspect-ratio: auto;
    object-fit: contain;
    background-color: transparent;
    border: 0;
    border-radius: 0;
  }

  /* ==========================================================================
     SHEET --lockup — THE LOGO WALL CARD.

     Restores the incumbent integration card exactly: one centred column holding
     the vendor lockup and its category chip, 1rem apart, all cards equal height.
     Owner call 2026-08-28, "make the design look exactly as is, with the new
     colours and theme" -- so the geometry is the incumbent's and every value
     that carries colour, radius or type comes from the chart layer.

     WHAT THIS REPLACES, AND THE DEFECT IT FIXES. The migrated card was a
     `.fh-sheet__figure--mark` band (a 3.5rem-tall logo) stacked over a
     `.fh-sheet__body` carrying an `<h3>` and the chip. But SIX of the eight
     source SVGs are full wordmark LOCKUPS -- micro-teams 195x30, jira 177x24,
     paloalto 132x24, pushOver 129x30, slack 88x30, splunk 84x24 -- so that `h3`
     printed a name the artwork already said. Every one of those cards showed its
     vendor name TWICE. Confirmed against the pre-migration baseline, which holds
     no standalone `Microsoft Teams`, `Pushover`, `Jira` or `Splunk` text line:
     the `h3` was introduced by the migration, not inherited.

     The two remaining SVGs are square ICONS -- sumo-logic-logo 57x57 and
     slack-webhook 36x36 -- which is exactly why the incumbent gave those two, and
     only those two, a text name beside the icon. `Sumo Logic` and `Slack Webhook`
     ARE in the baseline as their own lines, so they stay as text and copy parity
     holds.

     WIDTH IS NORMALISED, NOT HEIGHT, and that is deliberate rather than sloppy:
     `width: 100%; max-width: 200px` is the incumbent's `w-100 max-width:200px`,
     so each lockup is set to a common WIDTH and its height falls out of its own
     aspect. splunk (3.5:1) therefore stands taller than micro-teams (6.5:1) and
     the chips do not align across a row. They did not align in the incumbent
     either -- verified against the reference screenshot -- and matching it is the
     instruction. Do not "fix" this into a fixed height: that is
     `.fh-sheet__figure--mark`'s contract and it is the thing being replaced here.
     ======================================================================== */
  .fh-sheet--lockup {
    align-items: center;
    justify-content: center;
    gap: 1rem;
    padding: 1.5rem 1.25rem;
    /* The incumbent's `rounded-4`. Permitted on a bordered CARD by the owner
       decision at fh-tokens-landing.css:121; controls and chips stay on sm/md. */
    border-radius: var(--l-radius-card);
    /* `a { text-decoration: underline }` lives in the base layer and was reaching
       the card titles here. */
    text-decoration: none;
  }

  .fh-sheet__lockup {
    width: 100%;
    max-width: 200px;
    height: auto;
  }

  /* Mixed inline content -- an icon and a word -- so `align-items: center` puts
     the mark on the text's centre rather than its baseline, and `min-width: 0`
     lets the row shrink inside the track instead of pushing the card wide. */
  .fh-sheet__lockup-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
  }

  .fh-sheet__lockup--icon {
    max-width: 55px;
    flex: 0 0 auto;
  }

  /* The incumbent set this at Bootstrap's `fs-3` (1.75rem). --l-fs-d3 is the
     house step nearest it and, unlike a literal, it moves with the ramp. */
  .fh-sheet__lockup-name {
    color: var(--l-ink);
    font-family: var(--l-face-display);
    font-size: var(--l-fs-d3);
    font-weight: var(--l-fw-bold);
    letter-spacing: -0.01em;
    line-height: var(--l-lh-snug);
    /* THE WHOLE REASON `Slack Webhook` FITS ON ONE LINE.

       It was breaking to two inside a card that had room: measured 218px needed
       against 236px available. The cause is this element's DEFAULT `flex-shrink: 1`
       -- as a flex item in the lockup row it was being shrunk below its own content
       width, and the text wrapped to fill the smaller box.

       Found by testing candidates in the live page, not by reading the markup. Two
       earlier hypotheses were wrong and are worth recording so they are not retried:
       `width: max-content` on the ROW changed nothing (the row was never the
       constraint), and neither did `flex-shrink: 0` on the row or switching the card
       to `align-items: stretch`.

       `flex-shrink: 0` rather than `white-space: nowrap`: both give one line here,
       but nowrap would OVERFLOW a genuinely too-narrow card, while this only stops
       the premature shrink and still lets the text wrap when there is really no
       room. Verified: no overflow across the 320-1600 sweep. */
    flex-shrink: 0;
  }

  .fh-sheet__body {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.875rem;
    padding: 1.75rem;
    flex: 1 1 auto;
  }

  .fh-sheet__title {
    margin: 0;
    color: var(--l-ink);
    font-size: var(--l-fs-d2);
    font-weight: var(--l-fw-black);
    letter-spacing: -0.02em;
  }

  .fh-sheet__for {
    color: var(--l-gold);
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-semibold);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
  }

  .fh-sheet__body p {
    margin: 0;
    color: var(--l-sounding);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-relaxed);
  }

  .fh-sheet__actions {
    margin-block-start: auto;
    padding-block-start: 0.5rem;
  }

  /* ==========================================================================
     SHEET --compact — THE PARTNER CARD, returned to the incumbent's calm.

     Owner call 2026-08-28: the migrated card read "big and clumsy" against an
     incumbent that read "calm and easy to read". Four measured causes, and all
     four are geometry rather than palette:

       1. TITLE AT HEADLINE SCALE. `.fh-sheet__title` is `--l-fs-d2`, which caps
          at 34px. The incumbent set this title at ~17px. On a ~240px card a
          36-character title at 34px wraps to THREE lines, so the card read as a
          headline with a footnote under it.
       2. THE MARK HAD ITS OWN BAND. `.fh-sheet__figure--mark` gives it a
          full-width 3.5rem row, which reads as a separate region and pushes
          everything down. The incumbent put a small mark at the top-LEFT, inside
          the card's own padding, inline with the text column.
       3. THREE COLUMNS. `.fh-cols--2` resolves to 3 at the 1220px shell, so five
          partner cards ran 3 + 2 and the section ended ragged mid-width.
       4. A GHOST BUTTON. The incumbent's action was a full-width GOLD fill.

     SCOPED, NOT GLOBAL, and deliberately. `.fh-sheet__title` has 24 call sites
     across six templates -- including `components/chart/product_sheets.html`,
     `products/index` and `pricing/index`, whose cards are large primary product
     tiles where 34px is right. Shrinking the shared rule would have quietly
     flattened those. If the `/oss/` and `/love/` card walls want this calm too,
     add the modifier there; do not move it up into `.fh-sheet`.
     ======================================================================== */
  .fh-sheet--compact {
    border-radius: var(--l-radius-card);
  }

  /* NO BAND. The base rule gives the figure `background-color: var(--l-ground)`
     plus a closing hairline -- a recessed mat, which is right for a screenshot
     and wrong for a small mark: it made the logo read as its own region rather
     than as the first line of the card. The incumbent sat the mark directly on
     the card. */
  .fh-sheet--compact .fh-sheet__figure {
    padding: 1.25rem 1.25rem 0;
    background-color: transparent;
    border-block-end: 0;
  }

  /* THE ATTRIBUTION IS A SENTENCE, SO IT IS NOT TRACKED CAPS.

     These lines were on `.fh-label`, which uppercases and letterspaces, so
     `By Samuel Rossier` shipped as `BY SAMUEL ROSSIER`. DESIGN.md's The Tracked
     Caps Rule is explicit that the label register "never sets a heading, a
     sentence, or a paragraph" -- an attribution is a sentence. The incumbent had
     it in sentence case, which is also the calmer reading the owner asked for, so
     the fix and the request point the same way. */
  .fh-sheet--compact .fh-sheet__by {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin: 0;
    color: var(--l-sounding);
    font-family: var(--l-face-display);
    font-size: var(--l-fs-small);
    font-weight: var(--l-fw-semibold);
    letter-spacing: normal;
    text-transform: none;
  }

  /* Left-aligned and half the height: `object-position` is what moves a
     `contain`ed image inside its box, since `object-fit` alone leaves it centred. */
  .fh-sheet--compact .fh-sheet__figure--mark img {
    height: 2.25rem;
    width: auto;
    max-width: 65%;
    object-position: left center;
  }

  .fh-sheet--compact .fh-sheet__body {
    padding: 1.25rem;
    gap: 0.625rem;
  }

  /* A compact card with a figure gets less padding above its body, because the
     figure already supplied that space. Without this the two shapes of compact
     card -- one with a mark, one without -- would need different padding at the
     call site. `:has()` is already the mechanism `.fh-legend__item` uses for the
     same "does this row carry a symbol" question. */
  .fh-sheet--compact:has(.fh-sheet__figure) .fh-sheet__body {
    padding-block-start: 0.875rem;
  }

  /* ---- REPOSITORY / RECORD METRICS, AS ONE INLINE DATA ROW. ----

     These were four `.fh-chip--plain` boxes per card, which wrapped onto two and
     three rows and turned every card into a small pile of containers. The
     incumbent ran them as a single line of `label: value` pairs, which is denser,
     quieter, and reads as a readout rather than as a set of badges.

     THE SPLIT OF FACES IS THE POINT, and it is the rule rather than a flourish:
     the label is a word a person wrote, so it is Urbanist; the count was emitted
     by a system, so it is JetBrains Mono with tabular numerals. DESIGN.md, The
     Mono Is Measurement Rule: "Inside a technical figure, the numbers are mono
     and the words are not." Bright value against a `label`-toned key is also
     what gives the row its life -- the chips made every part the same weight.

     Copy-parity safe: the incumbent's markup was `<span>Stars: <span>3.4k</span>
     </span>` and the baseline records one line per metric, `'Stars: 3.4k'`, so a
     nested value extracts to the identical string. */
  .fh-sheet__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.875rem;
    margin: 0;
    color: var(--l-label);
    font-size: var(--l-fs-small);
  }

  .fh-sheet__meta b {
    color: var(--l-ink);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-data);
    font-variant-numeric: tabular-nums;
    font-weight: var(--l-fw-bold);
  }

  .fh-sheet--compact .fh-sheet__title {
    font-size: var(--l-fs-d3);
    font-weight: var(--l-fw-bold);
    letter-spacing: -0.01em;
    line-height: var(--l-lh-snug);
  }

  /* A title that is an identifier: see --l-fs-d4's note. Tracking returns to
     normal because a slug's underscores and hyphens are load-bearing and the
     display steps' negative tracking closes them up.

     TWO CLASSES, AND PLACED AFTER THE COMPACT TITLE RULE. Both matter, and both
     were found by measuring the computed size rather than reasoning about it. As
     a single `.fh-sheet__title--id` (0,1,0) it lost outright to
     `.fh-sheet--compact .fh-sheet__title` (0,2,0). Chained, the two tie at
     (0,2,0) -- and a tie is decided by ORDER, so while this sat earlier in the
     file it still lost. It has to come after. */
  .fh-sheet__title.fh-sheet__title--id {
    font-size: var(--l-fs-d4);
    letter-spacing: normal;
  }


  /* The incumbent's action filled the card.

     BOTH rules are needed, and the first is the one that actually does it.
     `.fh-sheet__body` is a column flex with `align-items: flex-start`, so
     `.fh-sheet__actions` was itself only 177px of a 233px content box -- and a
     `width: 100%` button inside it resolved against 177px, not the card.
     Measured before adding `align-self`, which is what caught it. */
  .fh-sheet--compact .fh-sheet__actions {
    align-self: stretch;
  }

  .fh-sheet--compact .fh-sheet__actions .fh-btn {
    width: 100%;
  }

  /* ==========================================================================
     FOOTER — the chart's imprint block.
     ======================================================================== */
  .fh-foot {
    border-block-start: var(--l-hairline) solid var(--l-neatline);
    padding-block: var(--l-space-16) var(--l-space-12);
  }

  /* Three tracks, not a flat run of columns: the brand lockup, the link groups,
     the newsletter. The link groups keep their own auto-fit run inside the
     middle track, so the two wide blocks cannot be squeezed to a link column's
     width and the link columns cannot be pulled out to a form's width.

     MEASURED at --l-container (1220px) less a 2.5rem gutter each side = 1140px
     of content, and the fr ratios are set to land on it:
       brand      208   the 125px mark, and "Expose Your Attack Surface" on one
                        line -- MEASURED at 201px, so 200 broke it in two
       links      612   four columns of 135 + three 24px gaps
       newsletter 256   an 8rem field + 8px + the 114px SUBSCRIBE button; at 272
                        with a 10rem field the button dropped to its own row
     plus the two 32px track gaps. 135 is the number to watch: "Terms &
     Conditions", the longest link in the footer, measures 129px. */
  .fh-foot__grid {
    display: grid;
    gap: var(--l-space-12) var(--l-space-8);
    grid-template-columns: minmax(0, 1fr);
  }

  @media (min-width: 62rem) {
    .fh-foot__grid {
      grid-template-columns:
        minmax(13rem, 0.97fr)
        minmax(0, 2.84fr)
        minmax(16rem, 1.19fr);
    }
  }

  /* Four columns at full width, dropping to three then two on its own as the
     track narrows -- the 8rem minimum is what decides, not a breakpoint. */
  .fh-foot__links {
    display: grid;
    gap: var(--l-space-12) 1.5rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr));
  }

  /* ---- The brand lockup. `align-items: flex-start` so the anchor around the
          mark is the width of the mark and not of the column -- a full-width
          invisible hit area over the claim text is a real bug, not a cosmetic
          one. ---- */
  .fh-foot__brand {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }

  .fh-foot__mark {
    display: inline-flex;
    margin-block-end: 0.75rem;
  }

  /* Sized off the HEIGHT with width:auto, the way .fh-nav__brand img is
     (34px there, one step up here). The source is 8134x2723 -- ratio 2.99, not
     the 5.77 the old width="150" height="26" attributes claimed -- so anything
     that pins the width either squashes the crest into an ellipse or, with
     `width: auto` and no height, lets the image fill its whole column: 231px at
     1440, 350px at 390. Both were measured on this footer. */
  .fh-foot__mark img {
    width: auto;
    height: 42px;
  }

  .fh-foot__claim {
    margin: 0;
    color: var(--l-ink);
    font-size: var(--l-fs-body);
    font-weight: var(--l-fw-bold);
  }

  .fh-foot__note {
    max-width: 34ch;
    margin: 0;
    color: var(--l-sounding);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-relaxed);
  }

  .fh-foot__title {
    margin: 0 0 1rem;
    color: var(--l-label);
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-bold);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
  }

  .fh-foot__list {
    display: flex;
    flex-direction: column;
    gap: 0.625rem;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .fh-foot__list a {
    color: var(--l-sounding);
    font-size: var(--l-fs-small);
    text-decoration: none;
  }

  .fh-foot__list a:hover {
    color: var(--l-gold);
    text-decoration: underline;
    text-underline-offset: 0.22em;
  }

  .fh-foot__imprint {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1rem;
    margin-block-start: var(--l-space-12);
    padding-block-start: 1.5rem;
    border-block-start: var(--l-hairline) solid var(--l-neatline);
    color: var(--l-label);
    font-size: var(--l-fs-data);
  }

  .fh-foot__imprint p {
    margin: 0;
  }

  .fh-foot__social {
    display: flex;
    flex-direction: row;
    gap: 1rem;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .fh-foot__social a {
    display: inline-flex;
    padding: 0.375rem;
    color: var(--l-label);
    text-decoration: none;
  }

  .fh-foot__social a:hover,
  .fh-foot__social a:focus-visible {
    color: var(--l-gold);
  }

  .fh-foot__icon {
    display: block;
    transition: transform var(--l-dur-fast) var(--l-ease);
  }

  .fh-foot__social a:hover .fh-foot__icon {
    transform: translateY(-1px);
  }

  .fh-foot__social a {
    color: var(--l-sounding);
    font-size: var(--l-fs-data);
    text-decoration: none;
  }

  .fh-foot__social a:hover {
    color: var(--l-gold);
    text-decoration: underline;
    text-underline-offset: 0.22em;
  }

  /* ==========================================================================
     FOOTER NEWSLETTER — the incumbent's third footer column: subscribe form
     and social marks, two titled groups stacked on the group gap. (Its Tools
     group, one link to /search/, is dropped by owner instruction -- Search is
     already the first item in the nav.)
     ======================================================================== */
  .fh-foot__news {
    display: flex;
    flex-direction: column;
    gap: var(--l-space-8);
    min-width: 0;
  }

  .fh-foot__form {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-block-start: 1.25rem;
  }

  /* Wraps rather than crushing the field: "Subscribe" is `white-space: nowrap`
     chrome at a fixed size, so shrinking is not an option for it and the field
     is what would give. Below an 8rem field the button drops to its own row. */
  .fh-foot__subscribe {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  /* Tighter than the standard control inset on both sides: the placeholder is
     14px MONO, which needs 105px, and at the component's own 0.8125rem inset a
     136px field gives it 104. It was clipping at "email addres". */
  .fh-foot__subscribe .fh-control {
    flex: 1 1 8rem;
    width: auto;
    padding-inline: 0.625rem;
  }

  /* Does not grow: when the footer stacks to one column the row is 700px wide,
     and a growing button took half of it. The field takes the slack instead. */
  .fh-foot__subscribe .fh-btn {
    flex: 0 1 auto;
    padding-inline: 1rem;
  }

  /* Standing on their own under a title, the 18px marks read as an afterthought;
     in the imprint row they were correctly small. Sized in CSS, not by swapping
     the attributes, so the intrinsic size stays the pre-CSS fallback. */
  .fh-foot__news .fh-foot__icon {
    width: 1.375rem;
    height: 1.375rem;
  }

  /* ==========================================================================
     MENU — a real disclosure. Button, aria-expanded, Escape, arrow keys, click
     away. The incumbent nav has zero keydown handlers site-wide; this is where
     that changes.
     ======================================================================== */
  .fh-menu {
    position: relative;
  }

  .fh-menu__trigger {
    display: inline-flex;
    align-items: center;
    gap: calc(var(--l-nav-mark) - var(--l-nav-mark-caret));
  }

  .fh-menu__trigger::after {
    content: "";
    width: var(--l-nav-mark-caret);
    height: var(--l-nav-mark-caret);
    border-inline-end: var(--l-hairline) solid currentColor;
    border-block-end: var(--l-hairline) solid currentColor;
    rotate: 45deg;
    translate: 0 -2px;
    transition: rotate var(--l-dur-fast) var(--l-ease);
  }

  .fh-menu__trigger[aria-expanded="true"] {
    color: var(--l-gold);
  }

  .fh-menu__trigger[aria-expanded="true"]::after {
    rotate: -135deg;
    translate: 0 1px;
  }

  .fh-menu__panel {
    position: absolute;
    inset-block-start: 100%;
    inset-inline-start: -0.75rem;
    z-index: var(--l-z-dropdown);
    display: flex;
    flex-direction: column;
    min-width: 14rem;
    padding: 0.5rem;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-contour-hi);
    border-radius: var(--l-radius-md);
  }

  .fh-menu__panel[hidden] {
    display: none;
  }

  .fh-menu__panel a {
    padding: 0.5rem 0.625rem;
    border-radius: var(--l-radius-sm);
    color: var(--l-sounding);
    font-size: var(--l-fs-data);
    text-decoration: none;
  }

  .fh-menu__panel a:hover {
    background-color: var(--l-shoal);
    color: var(--l-ink);
  }

  /* ==========================================================================
     DIALOG — the accessible replacement for the trapped enterprise modal.

     The dialog markup still carries Bootstrap's .modal/.modal-dialog classes so
     it keeps its incumbent look on layouts/base.html. This layout ships no
     Bootstrap CSS at all, so the overlay geometry is restated here; the two
     stylesheets never load on the same page. Behaviour lives in the dialog's own
     script (Escape, a Tab cycle confined to the dialog, focus returned to the
     opener) because base.html does not load js/fh-ui.js.
     ======================================================================== */
  [data-fh-dialog] {
    display: none;
    position: fixed;
    inset: 0;
    z-index: var(--l-z-modal, 1055);
    overflow-y: auto;
    outline: none;
  }

  [data-fh-dialog-backdrop] {
    position: fixed;
    inset: 0;
    z-index: calc(var(--l-z-modal, 1055) - 1);
    background-color: var(--l-scrim);
  }

  /* The dialog's own plate. This layout ships no .card or .modal-content rule of
     any kind, so without this the dialog's copy would be printed straight onto
     the scrim over whatever happened to be behind it. It is the house container
     and nothing more: the `deep` substrate, one hairline, the card radius. No
     shadow -- the Flat Sheet Rule is absolute, and the hairline is what detaches
     a forward surface on this ground. Measured on this plate: --l-ink 16.4:1,
     --l-sounding 13.0:1 (the inherited body ink), --l-label 9.9:1, --l-gold
     10.5:1. */
  [data-fh-dialog] .modal-content {
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-card);
  }

  /* A real focusable control, not the bare <i> it replaced. The reset keeps the
     incumbent icon-only look; the ring is what makes it usable by keyboard. */
  .fh-dialog-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    min-height: 2rem;
    border-radius: 50%;
    cursor: pointer;
  }

  .fh-dialog-close:focus-visible {
    outline: 2px solid var(--l-gold, currentColor);
    outline-offset: 2px;
  }

  /* A ruled footer row on a data block: the chart's note under a table. */
  .fh-block__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem 1rem;
    border-block-start: var(--l-hairline) solid var(--l-neatline);
  }

  /* ==========================================================================
     FORM FIELD SET — the chart's data-entry block.

     WHY THIS EXISTS SEPARATELY FROM .fh-entry__*
     `.fh-entry__*` is the hero position-entry field: ONE input, sized and
     coloured to be the loudest element on the page, with a submit button
     welded into its own border and a combobox listbox hanging off it. Six of
     those stacked in a contact form would out-shout the page they sit on and
     invert the hierarchy the direction sets deliberately. This is the quiet
     counterpart: same materials, same border ladder, one third the voice.
     Do not stretch either one to cover the other.

     Authored once here because four page tasks across CP5/CP6/CP7 need it
     (contact_us, request_demo, public-report-oem-page, newsletter, the two
     auth/ pages, and request_enterprise_demo_modal). Markup contract, and it
     is a contract -- a control without all three of these is not done:
       1. a VISIBLE <label class="fh-label">, never a placeholder standing in
          for one (placeholders vanish on input and are not exposed reliably);
       2. a PROGRAMMATIC association -- label[for] == control[id];
       3. a KEYBOARD-REACHABLE focus ring, which comes free from the unlayered
          :focus-visible rule in fullhunt-tokens.css. Nothing here may set
          `outline: none` on a control.
     `components/chart/form.html` wraps all three into macros so a page cannot
     forget one; hand-written markup is allowed but must match.

       <div class="fh-field">
         <label class="fh-label fh-label--tight" for="email">Work email</label>
         <input class="fh-control" id="email" name="email" type="email"
                autocomplete="email" required />
         <p class="fh-field__hint" id="email-hint">We reply within one day.</p>
       </div>

     MEASURED CONTRAST (this block adds no new colour token):
       label   --l-label    on --l-ground  10.71:1
       value   --l-ink      on --l-deep    16.37:1
       hint    --l-label    on --l-ground  10.71:1
       holder  --l-label    on --l-deep     9.87:1  (matches .fh-entry__input's
                                                     existing placeholder)
     KNOWN GAP, reported not silently diverged from: the resting control border
     --l-contour-hi is 2.54:1 on --l-deep, under WCAG 1.4.11's 3:1 non-text
     floor. It is used anyway because it is exactly what .fh-entry__form paints,
     and giving forms a second, louder field vocabulary would be worse than the
     gap. Raising it means raising the hero field too -- one decision, one
     token, owner's call, not a form component's to make unilaterally.
     ======================================================================== */
  .fh-form {
    display: flex;
    flex-direction: column;
    gap: var(--l-space-8);
    width: 100%;
    max-width: 42rem;
    min-width: 0;
  }

  /* ---- The form's own head: heading, optional standfirst, and the rule that
          closes them off.

     These were `.fh-panel__title` / `.fh-panel__body` at the form call sites --
     panel children used outside a panel. That borrowing is also what kept those
     forms wrapped in a `.fh-panel` they must not be in: `.fh-control` fills
     itself with --l-deep, `.fh-panel` IS --l-deep, so every field inside one
     computes to the same colour as its container and survives only as a
     2.54:1 border (fh-chart.css's auth-sheet note states the rule; measured on
     /request-demo/ at 1440px, form panel and control were both
     rgb(10, 26, 38)). Spelled here so a form printed on the ground owns a
     heading instead of borrowing one.

     A RULE, NOT A BOX. `.fh-form` is a flex column on a 2rem gap, which is the
     right rhythm between field groups and far too much between a heading and
     its own standfirst -- so the pair is one child on its own 0.5rem gap, and
     the hairline beneath it is what bounds the form. That is how this chart
     contains a block: a neatline, not a colour change (the Flat Sheet Rule). ---- */
  .fh-form__head {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding-block-end: 1.25rem;
    border-block-end: var(--l-hairline) solid var(--l-neatline);
  }

  .fh-form__title {
    margin: 0;
    color: var(--l-ink);
    font-size: var(--l-fs-d3);
    font-weight: var(--l-fw-bold);
    letter-spacing: -0.01em;
  }

  .fh-form__note {
    margin: 0;
    color: var(--l-sounding);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-relaxed);
  }

  .fh-fields {
    display: grid;
    gap: 1.25rem 1.5rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  }

  /* A field that must not be halved by .fh-fields' auto-fit: a message body,
     a full-width consent row. */
  .fh-fields > .fh-field--wide {
    grid-column: 1 / -1;
  }

  .fh-field {
    display: flex;
    flex-direction: column;
    gap: 0.4375rem;
    min-width: 0;
  }

  /* .fh-label is a flex row with a 0.75rem gap, sized for the marginalia use
     (label text, then the trailing hairline). As a FORM label that gap lands
     between the field name and its required mark and reads as a detached
     bullet twelve pixels away. Scoped to form labels only -- the marginalia
     spacing elsewhere is untouched. */
  .fh-field > .fh-label {
    gap: 0.25rem;
  }

  /* The required mark is a real character next to the visible label, not a
     colour on its own. It is aria-hidden in the macro because `required` on
     the control is what actually carries the meaning to AT -- the asterisk is
     for people reading the page, not for the accessibility tree. */
  .fh-field__req {
    color: var(--l-gold);
    font-weight: var(--l-fw-bold);
  }

  /* ---- The shared control surface: input, textarea and select all wear it,
          so a form is one ruled ladder and not three. ---- */
  .fh-control {
    display: block;
    width: 100%;
    min-width: 0;
    padding: 0.6875rem 0.8125rem;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-contour-hi);
    border-radius: var(--l-radius-sm);
    color: var(--l-ink);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-body);
    line-height: var(--l-lh-normal);
    transition: border-color var(--l-dur-fast) var(--l-ease);
  }

  .fh-control::placeholder {
    color: var(--l-label);
    font-size: var(--l-fs-data);
  }

  .fh-control:hover {
    border-color: var(--l-land-edge);
  }

  /* Focus paints the border gold as well as taking the unlayered gold ring,
     so a pointer user gets the same state change a keyboard user does. The two
     read as a doubled neatline, which is a chart's own idiom -- and critically,
     nothing here suppresses the ring: the unlayered :focus-visible rule in
     fullhunt-tokens.css stays in charge of keyboard focus. */
  .fh-control:focus {
    border-color: var(--l-gold);
  }

  textarea.fh-control {
    min-height: 8rem;
    resize: vertical;
  }

  /* appearance:none because the OS select chrome ignores this palette and
     lands a light-grey widget in the middle of a night chart. The chevron is
     the same 45deg corner .fh-menu__trigger draws, inlined as a data URI
     because a <select> cannot carry a pseudo-element reliably. */
  select.fh-control {
    appearance: none;
    padding-inline-end: 2.25rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 7' fill='none' stroke='%23a9c6d6' stroke-width='1.4'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.8125rem center;
    background-size: 0.625rem 0.4375rem;
  }

  /* The invalid state rides the attribute, not a class, so the CSS cannot get
     out of step with what assistive tech is told. Critical red is 4.71:1 on
     --l-deep: fine for a mark or a rule, under the label floor, so it never
     sets text here either -- The Severity Ink Rule, same as .fh-chip--caution. */
  .fh-control[aria-invalid="true"],
  .fh-control[aria-invalid="true"]:hover {
    border-color: var(--fh-sev-critical);
  }

  .fh-field__hint {
    margin: 0;
    color: var(--l-label);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-normal);
  }

  /* Same triangle mark as .fh-entry__error, deliberately: one error shape on
     the site. Shape carries the meaning so the 4.71:1 red never has to. */
  .fh-field__error {
    display: flex;
    align-items: center;
    gap: 0.4375rem;
    margin: 0;
    color: var(--l-ink);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-data);
  }

  .fh-field__error:empty {
    display: none;
  }

  .fh-field__error::before {
    content: "";
    flex: none;
    inline-size: 0.5rem;
    block-size: 0.5rem;
    background-color: var(--fh-sev-critical);
    clip-path: polygon(50% 0, 100% 100%, 0 100%);
  }

  /* ---- Checkbox / radio. The native control is kept and tinted: it already
          has the keyboard behaviour, the indeterminate state, the AT role and
          the platform hit-target. A div with a tick in it has none of that. ---- */
  .fh-check {
    display: flex;
    align-items: flex-start;
    gap: 0.625rem;
    color: var(--l-sounding);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-normal);
    cursor: pointer;
  }

  .fh-check__box {
    flex: none;
    inline-size: 1.0625rem;
    block-size: 1.0625rem;
    margin: 0.1875rem 0 0;
    accent-color: var(--l-gold);
    cursor: pointer;
  }

  .fh-form__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.875rem;
  }

  /* ==========================================================================
     FORM STATUS BANNER — the block-level counterpart to .fh-field__error.

     Not .fh-notice (that is a Notices-to-Mariners feed row) and not
     .fh-entry__error (that is one field's inline mark). This is the banner a
     form replaces itself with, or puts above itself, after a submit.

     State is carried ENTIRELY by the mark: its SILHOUETTE (disc for done,
     triangle for failed, bar for working) and then its hue. Shape first means
     the three states are still told apart with the colour removed, so the hue
     never has to carry the message on its own.

     NO SIDE BAR. This first shipped with a 3px coloured border-inline-start
     and that was wrong twice over. It encoded nothing the mark did not already
     encode -- decoration claiming to be information -- and a thick coloured
     rule down one edge of a box is the single most recognisable tell of a
     generated interface. It also broke the world's own rule: emphasis in the
     Night Chart comes from colour and hairlines, never from thickness or blur.
     The banner is a hairline-ruled surface like every other surface here.
     Do not add it back, and do not reach for a thicker border to make a state
     louder -- change the mark.

     MEASURED, all on --l-deep:
       message text  --l-ink            16.37:1  (house floor ~10:1)
       done mark     --fh-success        6.62:1  } non-text marks: the floor is
       failed mark   --fh-sev-critical   4.71:1  } WCAG 1.4.11's 3:1, which all
       working mark  --l-gold           10.51:1  } three clear. None sets text.

     --fh-success is a shared fullhunt-tokens.css token, not an --l-* one, on
     purpose: the landing scale has no success colour (it carries gold, caution
     violet and a critical channel form, and nothing green), and the shared file
     names --fh-success for exactly this job. It is also the house pattern --
     .fh-risk--low/medium/high/critical and .fh-entry__error already take the
     shared --fh-sev-* ramp directly. This is not accidental drift.

       <p class="fh-alert fh-alert--ok" role="status">Thanks -- we got it.</p>
       <p class="fh-alert fh-alert--error" role="alert">That email looked wrong.</p>

     role="status" for success (polite), role="alert" for failure (assertive).
     A banner that appears after submit must also be focused or announced;
     role alone does not move the cursor.
     ======================================================================== */
  .fh-alert {
    display: flex;
    align-items: flex-start;
    gap: 0.6875rem;
    margin: 0;
    padding: 0.875rem 1rem;
    background-color: var(--l-deep);
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-sm);
    color: var(--l-ink);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-normal);
  }

  .fh-alert::before {
    content: "";
    flex: none;
    inline-size: 0.625rem;
    block-size: 0.625rem;
    margin-block-start: 0.28125rem;
    background-color: var(--l-label);
  }

  .fh-alert--ok::before {
    background-color: var(--fh-success);
    border-radius: 50%;
  }

  .fh-alert--error::before {
    background-color: var(--fh-sev-critical);
    clip-path: polygon(50% 0, 100% 100%, 0 100%);
  }

  /* Working / in-flight. A bar, not a spinner: the reduced-motion guard would
     freeze a spinner mid-turn and leave a stalled graphic claiming progress. */
  .fh-alert--busy::before {
    background-color: var(--l-gold);
    block-size: 0.1875rem;
    margin-block-start: 0.5rem;
  }

  /* ==========================================================================
     DISCLOSURE — the accordion, as <details>/<summary>.

     Native on purpose. `base_v2` loads no Bootstrap JS, so a ported
     `.collapse` panel is either permanently open (layout blows up) or
     permanently closed -- and permanently closed reads as DELETED COPY to the
     copy-parity gate, which diffs visible text. <details> keeps the copy in
     the accessibility tree, gives Enter/Space toggling, the expanded state and
     the AT role for free, and needs no script to survive.

       <details class="fh-disclose">
         <summary class="fh-disclose__summary">Can I bring my own seed list?</summary>
         <div class="fh-disclose__body"><p>Yes...</p></div>
       </details>

     Do not add `open` to more than one in a set unless the copy needs it, and
     do not rebuild this in Alpine.
     ======================================================================== */
  .fh-disclose {
    border-block-end: var(--l-hairline) solid var(--l-neatline);
  }

  .fh-disclose:first-of-type {
    border-block-start: var(--l-hairline) solid var(--l-neatline);
  }

  .fh-disclose__summary {
    display: flex;
    align-items: center;
    gap: 0.875rem;
    padding: 1.0625rem 0.25rem;
    color: var(--l-ink);
    font-size: var(--l-fs-d3);
    font-weight: var(--l-fw-semibold);
    line-height: var(--l-lh-snug);
    cursor: pointer;
    list-style: none;
  }

  /* Both spellings: ::-webkit-details-marker for older WebKit, list-style for
     everything current. Without both, the default triangle sits next to ours. */
  .fh-disclose__summary::-webkit-details-marker {
    display: none;
  }

  .fh-disclose__summary:hover {
    color: var(--l-gold);
  }

  /* The same 45deg corner as .fh-menu__trigger, so "this opens" is one shape
     across the whole site. margin-inline-start:auto pins it to the far edge. */
  .fh-disclose__summary::after {
    content: "";
    flex: none;
    margin-inline-start: auto;
    width: 0.4375rem;
    height: 0.4375rem;
    border-inline-end: var(--l-hairline) solid currentColor;
    border-block-end: var(--l-hairline) solid currentColor;
    rotate: 45deg;
    translate: 0 -2px;
    transition: rotate var(--l-dur-fast) var(--l-ease);
  }

  .fh-disclose[open] > .fh-disclose__summary {
    color: var(--l-gold);
  }

  .fh-disclose[open] > .fh-disclose__summary::after {
    rotate: -135deg;
    translate: 0 1px;
  }

  .fh-disclose__body {
    padding: 0 0.25rem 1.25rem;
    max-width: var(--l-measure);
    color: var(--l-sounding);
    line-height: var(--l-lh-relaxed);
  }

  .fh-disclose__body > * + * {
    margin-block-start: 0.875rem;
  }

  /* ==========================================================================
     PLATE PATTERN — a decorative sheet pattern clipped to its plate.

     Lives in `components` rather than beside .fh-plate in `layout` because it
     is a surface treatment, and because components outranks layout, so the
     `overflow` override lands without a specificity fight.

     Why an image element at all: the incumbent shipped bg_pattern_1.svg behind the
     enterprise CTA using Bootstrap's `position-absolute w-100 top-0 start-0`,
     none of which base_v2 loads -- so on a migrated page it was rendering as a
     full-width in-flow block image, shoving the copy down. Deleting it would
     have been the tidy fix and the wrong one: `img` is a GATED count in
     goal_motion_parity, and the standing instruction is that graphics do not
     decrease. So it is kept and actually positioned. No blur, no shadow: the
     pattern is flattened into the sheet by opacity, which is colour, not depth.

     It also happens to fit: bg_pattern_1.svg is sweeping curves stopping
     between #FFD012 and #BB41FD -- amber to violet, the two accents this
     palette already owns, and the curves read as survey track lines. The
     opacity is held low because violet is RESERVED for caution here, and a
     decorative violet field at full strength would start competing with the
     things that are allowed to mean caution.

     `isolation: isolate` is load-bearing, not tidiness. `.fh-plate` is
     position:relative with z-index:auto, so it is NOT a stacking context; a
     z-index:-1 child would escape it, fall behind <body>, and be painted over
     by body's own graticule background -- the pattern would silently vanish.
     Isolating the plate traps the negative layer inside it.
     ======================================================================== */
  .fh-plate--patterned {
    position: relative;
    isolation: isolate;
    overflow: hidden;
  }

  .fh-plate__pattern {
    position: absolute;
    inset: 0;
    z-index: var(--l-z-traverse);
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    opacity: 0.15;
    pointer-events: none;
  }

  /* ==========================================================================
     ASSET SCORE SHIELD — the grade badge, ported from apps/asm.

     ASM gives this a whole column at 5-6rem. Here it sits in a badge row beside
     `LIVE` at 3.25rem — doubled from the first pass by owner call 2026-08-30,
     which put it at the badges' cap height and made the grade unreadable at a
     glance. It now deliberately out-sizes the row: the grade is the one thing
     on the header that ranks the whole record, so it is allowed to be the
     tallest thing in it. `object-fit: contain` because the nine SVGs are
     310x345 and not all the same aspect.

     `display: inline-block` + `vertical-align: middle`, because fh-reset.css
     sets `img { display: block }` and a block child takes its own line —
     DESIGN.md's Mixed Inline Content rule. Harmless inside the flex rows that
     are its usual home, load-bearing anywhere else.
     ========================================================================== */
  .fh-score {
    display: inline-block;
    vertical-align: middle;
    inline-size: 3.25rem;
    block-size: 3.25rem;
    flex: 0 0 auto;
    object-fit: contain;
  }

  /* ==========================================================================
     NOTICE BAR — a dismissible message with one action, on one line.

     Authored here rather than as page CSS in search.html because it is a
     component, not a page tweak: the playbook's own guidance is that anything
     past a handful of page rules belongs in the shared layer. Today it has one
     caller (components/community-banner.html, included by search/search.html),
     but the shape — message, action, dismiss — is the generic one.

     The message takes the free space so the action and the dismiss are pushed
     to the right edge; `flex-wrap` lets the action drop under a long message on
     a narrow viewport while the dismiss stays pinned to the top corner.
     ========================================================================== */
  /* `width: 100%` because the flash region nests this inside `.fh-stack`, which
     is `align-items: flex-start` and therefore shrink-wraps every child — the
     notice would size to its text and the action would sit hard against the
     message instead of at the right edge. No effect on the community banner,
     where it is already a block-level child of `.fh-shell`. */
  .fh-notice {
    display: flex;
    width: 100%;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem 1.5rem;
    padding: 1.125rem 1.25rem;
  }

  /* `flex: 1` with a 20rem basis: it eats the slack on a wide viewport, which
     is what moves the dismiss to the corner, and it is the first thing to wrap
     when there is not enough room for all three items. */
  .fh-notice__text {
    flex: 1 1 20rem;
    min-width: 0;
    /* A notice, not a headline. `.fh-panel__title` is the component's one
       sanctioned heading-size override and lands at 22px/700, which is louder
       than the h1 further down the page and made a dismissible bar read as the
       page's most important statement. */
    font-size: var(--l-fs-lead);
    font-weight: var(--l-fw-semibold);
    line-height: var(--l-lh-snug);
  }

  .fh-notice__action {
    flex: 0 0 auto;
  }

  /* Pinned to the top of the row rather than centred on it, so a message that
     wraps to two or three lines does not drag the dismiss down to its middle.
     The negative margins pull it into the panel's padding so the glyph sits in
     the actual corner instead of one gutter inside it. */
  .fh-notice__dismiss {
    flex: 0 0 auto;
    align-self: flex-start;
    margin-block-start: -0.375rem;
    margin-inline-end: -0.375rem;
    padding: 0.375rem;
    color: var(--l-label);
  }

  .fh-notice__dismiss:hover {
    color: var(--l-ink);
  }
}

/* ---------------------------------------------------------------------------
   UNLAYERED, deliberately. core/.../fullhunt-tokens.css declares :focus-visible
   outside any layer so its ring beats all layered CSS -- that is the intended
   contract. Here the wrapper already paints focus across the whole control
   (gold border plus a 3px ring), so the input's own 2px outline stacked a third
   gold rectangle inside the field. This defers the inner ring to the outer one,
   and only while the wrapper is actually painting: if that rule ever goes away,
   :focus-within stops matching and the native ring returns on its own.
   ------------------------------------------------------------------------- */
.fh-entry__form:focus-within .fh-entry__input:focus-visible {
  outline: none;
  border-radius: 0;
}
