/* ============================================================================
   CONSOLE CHROME — the only console-specific layer.

   Everything that makes a FullHunt surface look like a FullHunt surface now
   comes from landing's own stylesheets, copied verbatim into this directory:

     fh-tokens-chart.css  fh-reset.css  fh-base.css  fh-layout.css
     fh-components.css    fh-chart.css  fh-utilities.css

   That is deliberate and it is the whole point. The owner's instruction was
   "make search and host identical to landing. PERIOD", and the only way a port
   of landing's templates is genuinely identical is if it resolves against
   landing's actual rules rather than against a re-derived subset that drifts
   the first time either side is edited. When landing changes, these files are
   re-copied; there is no second opinion to reconcile.

   What is left here is the part landing has no equivalent for: console is an
   app with a sidebar, a topbar and 40-odd Bootstrap-era pages that have not
   been rebuilt yet. Three jobs only:

     1. Hold the app chrome to a sane size against landing's marketing type
        scale, which is built for a page whose h1 is the loudest thing on it.
     2. Bring the incumbent chip families onto the system centrally, so all 24
        unmigrated templates move at once.
     3. Fill the small gaps landing's shell covers in its own markup.

   Do NOT add component rules here. If search or host needs a rule, it belongs
   in landing's file and gets copied across, or the two surfaces have already
   started to drift.
   ========================================================================= */

@layer components {
  /* The Night Chart graticule belongs to search and host readouts. Product,
     authentication, and error surfaces use the platform ground from DESIGN.md. */
  body.console-platform,
  body.auth-page,
  body.error-shell {
    background-image: none;
  }

  /* =========================================================================
     0. THE RESET UN-BUILT EVERY BOOTSTRAP BUTTON IN THE APP.

     `fh-reset.css` — landing's, copied in verbatim — carries:

         button { padding: 0; background: none; border: 0; cursor: pointer; }

     On landing that is correct and load-bearing: there is no button framework
     there, every `<button>` is authored, and the comment above it records a real
     defect it fixed. In console it is a catastrophe, and for a reason that is
     easy to miss: `@layer reset` sorts ABOVE `@layer vendor`, and a LAYERED
     ELEMENT SELECTOR BEATS A LOWER-LAYERED CLASS SELECTOR regardless of
     specificity. So `button` outranks `.btn`, and Bootstrap's padding and border
     were stripped from every button in the console.

     What shipped: "Save Changes" and "Change Password" on the settings page
     rendered as gold TEXT with the fill hugging the glyphs — no box, no padding.
     The fill survived at all only because `.btn-fh-primary` sets
     `background: ... !important`, and `!important` inverts layer order. So the
     colour came through and the geometry did not, which is the worst of both
     and looks exactly like a broken stylesheet.

     NONE OF MY GATES CAUGHT IT. axe reported 0 on that page, there was no
     overflow, and no console error — a button with no padding is still a button
     with an accessible name. It took looking at the page. That is the lesson,
     not the rule.

     `revert-layer` DOES NOT WORK HERE, and it was my first attempt. It rolls a
     property back to the value from the PREVIOUS layer — which is still
     `reset`'s `padding: 0`, because reset is what beat vendor in the first
     place. It resolved to the very declaration it was meant to escape and
     changed nothing. Verified: every button on all 15 pages still measured
     `padding: 0px`.

     The values have to be re-declared. Bootstrap drives `.btn` entirely through
     its own custom properties, and `.btn-sm` / `.btn-lg` / `.btn-primary` work
     by overriding those properties rather than the declarations — so restating
     the declarations in terms of the same variables restores every variant at
     its correct size, instead of freezing one and breaking the other two.
     ====================================================================== */
  .btn {
    padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
    border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
    background-color: var(--bs-btn-bg);
    border-radius: var(--bs-btn-border-radius);
  }

  /* `.btn p-0` is a deliberate Bootstrap opt-out (the topbar avatar trigger).
     Honour it rather than forcing padding back on. */
  .btn.p-0 { padding: 0; }
  .btn.border-0 { border: 0; }
  .btn.bg-transparent { background-color: transparent; }

  /* Bootstrap's checkbox and radio ship `#0d6efd` — its own blue, in a palette
     that has no blue. The subscribe checkbox on the settings page was the most
     visible instance. */
  .form-check-input:checked {
    background-color: var(--l-gold);
    border-color: var(--l-gold);
  }

  .form-check-input:focus {
    border-color: var(--l-gold);
    box-shadow: 0 0 0 0.2rem rgb(var(--l-gold-rgb) / 0.25);
  }

  /* The same reset sets `img, svg, video { display: block }`, which is right on
     a page of authored figures and wrong inside a button, a badge or a table
     cell, where an icon sits ON the text baseline. Returned to inline flow
     wherever it is a mark rather than a figure. */
  .btn img, .btn svg,
  .badge-fh img, .badge-fh svg,
  .nav-link img, .nav-link svg,
  td img, td svg,
  .user-avatar img, .user-avatar-sm img {
    display: inline-block;
    vertical-align: middle;
  }

  /* =========================================================================
     0b. THE SAME RESET UN-BUILT EVERY *AUTHORED* BUTTON TOO.

     Section 0 above repaired `.btn`, and stopped there. It shouldn't have. The
     reset's two rules are

         input, button, textarea, select { font: inherit; color: inherit }
         button { padding: 0; background: none; border: 0; cursor: pointer }

     and `font` is a SHORTHAND, so it resets font-size, font-weight AND
     font-family in one go. Every `<button>` in the console that is not a
     Bootstrap `.btn` — and style.css styles four families of them — therefore
     lost its padding, its border and its type, while keeping only the
     properties the shorthand does not cover.

     THAT ASYMMETRY IS THE TELL, and it is how these were finally found. The
     sidebar's group headers still rendered UPPERCASE with 1.6px of tracking,
     because `text-transform` and `letter-spacing` are not part of `font`. So
     they looked deliberate — just wrong — rather than looking unstyled. Measured
     at 1440x1000 on /: `padding 0px`, `font-size 16px`, `font-weight 400`,
     against the `0.75rem 1.5rem 0.25rem / 0.75rem / 700` style.css declares.
     Five per page across all 14 routes, 70 instances.

     The 1.6px is itself the proof: `0.1em` of tracking resolved against 16px,
     not against the 12px the rule asks for. A computed value that is internally
     consistent with the WRONG font size is stronger evidence than a screenshot.

     WHY THESE FOUR AND NOT A BLANKET `button { padding: revert }`. Because the
     reset's rule is correct for the many buttons that genuinely are bare
     triggers — the topbar avatar, the disclosure carets, `.btn.p-0`. Undoing it
     wholesale would re-break what section 0 explicitly honours. Each family
     below is named, and each re-declares only what the shorthand took.

     `.sidebar-overlay` is deliberately absent: it is a full-viewport scrim with
     no content, so padding and type are meaningless on it.
     ====================================================================== */

  /* =========================================================================
     0d. THE INTEGRATION CATALOGUE.

     SPACING TOKENS: USE `--space-N` FOR N <= 6, `--l-space-N` FOR N >= 8.
     Building this section surfaced a defect that predates it. `--l-space-2`,
     `--l-space-3` and `--l-space-4` ARE NOT DEFINED ANYWHERE -- the `--l-space-*`
     scale in fh-tokens-chart.css starts at 8, and its own comment says it
     "extends the shared --space-* scale, which stops at 2.5rem". So the small
     steps live on `--space-*` and only the large ones on `--l-space-*`, and a
     `var(--l-space-2)` silently computes to nothing: `gap` fell back to `normal`
     and `padding` to `0px`, measured. Nine such declarations were already in this
     file, including `.topbar { gap }`, and landing never had the bug because it
     uses `--space-*` for these steps. All twenty call sites are now repointed.
     Two prefixes for one conceptual scale is the trap; the boundary is 6/8.

     Thirty cards, six sections. The template's own header carries the argument
     for why cards were the right answer to this brief at all; this is geometry.

     `auto-fit` with a 19rem minimum, NOT a fixed column count. Thirty items
     across six sections of 4-7 means no single column count divides them all,
     so a fixed grid would leave a partial row in most sections at most widths.
     THE EMPTY CELL RULE then decides how the hairline is drawn: the frame is an
     `outline` ON THE CARD, never the container's ground showing through a gap,
     so an unoccupied cell in a partial last row paints exactly what an occupied
     one paints and is invisible at every count and every width. `outline` rather
     than `box-shadow` because the Flat Sheet Rule bans box-shadow outright, and
     an outline takes no space, which is what lets adjacent cards share one
     hairline across the 1px gap.
     ====================================================================== */
  .integrations-section {
    margin-block-end: var(--l-space-8);
  }

  .integrations-section__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2) var(--space-4);
    margin-block-end: var(--space-4);
    padding-block-end: var(--space-3);
    border-block-end: var(--l-hairline) solid var(--l-neatline);
  }

  .integrations-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
    gap: var(--l-hairline);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .integration-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-5);
    /* A grid item defaults to `min-width: auto`, which is its CONTENT's minimum
       -- and `white-space: nowrap` on a 54-character endpoint path makes that
       minimum wider than the column. The item then overflows its own outline
       into the neighbouring card, which is what shipped on first render:
       `/api/v1/vulnerability-intelligence/vulnerabi` ran straight through the
       frame. `min-width: 0` lets the item be narrower than its content so the
       endpoint's own `overflow-x` can do its job. */
    min-width: 0;
    background-color: var(--l-deep);
    outline: var(--l-hairline) solid var(--l-neatline);
    /* Two channels move on hover, not one: the frame goes gold AND the ground
       lifts to `shoal`. No transform, no shadow, no lift -- the Flat Sheet Rule
       holds here as everywhere. */
    transition: outline-color var(--l-dur-fast) var(--l-ease),
                background-color var(--l-dur-fast) var(--l-ease);
  }

  .integration-card:hover {
    outline-color: var(--l-gold);
    background-color: var(--l-shoal);
  }

  /* The whole card is the hit area, via a stretched pseudo-element on its one
     anchor. This is the technique the Recent Searches row could NOT use --
     there, `.truncate` put `overflow: hidden` on an ancestor, which clips an
     absolutely-positioned descendant. Nothing here hides overflow, so the
     honest CSS version works and no JavaScript is needed. One anchor per card
     means one tab stop, a real URL in the status bar, and working middle-click. */
  .integration-card__link {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    text-decoration-line: none;
  }

  .integration-card__link::after {
    content: "";
    position: absolute;
    inset: 0;
  }

  .integration-card {
    position: relative;
  }

  /* Matched to `.quick-action-symbol`, the same pattern on the dashboard: a mark
     beside a title in a list row. It sizes at `1rem` inside a fixed `1.25rem`
     gutter, and that pairing is doing two jobs -- the size is the documented body
     step rather than a literal, and the fixed width means every title starts on
     the same optical line regardless of how wide its glyph happens to be. A
     bare font-size leaves the titles ragged across a row, because a clock glyph
     and a shield glyph are not the same width.

     The first version here was a literal `1.125rem` with no gutter, invented
     with no precedent in this file. The design hook caught the literal; reading
     the sibling component is what supplied the right answer. */
  .integration-card__symbol {
    flex: 0 0 auto;
    width: 1.375rem;
    color: var(--l-gold);
    font-size: var(--l-fs-d4);
    line-height: 1;
    text-align: center;
  }

  /* THE CARD HAD FOUR FONT SIZES INSIDE 3px OF EACH OTHER -- 16 / 15 / 14 / 13
     in a 373px box, measured. Four steps that close is not a hierarchy; every
     element reads as the same rank and the eye has nowhere to land, which is
     what "hard to read" was. The craft floor asks for obvious scale AND weight
     steps, and this had neither.

     Now three tiers with a real gap between them, plus a fourth channel that is
     not size at all:
       title     18px / 700   ink        the name, and clearly the name
       body      15px / 400   sounding   prose, at relaxed leading
       endpoint  13px / mono  label      distinguished by FACE, not size
       chips     13px / 600   caps       the label register's own floor
     The endpoint dropping to 13 is safe precisely because mono already sets it
     apart -- it does not have to win on size to read as a different kind of
     thing. */
  .integration-card__title {
    color: var(--l-ink);
    font-size: var(--l-fs-d4);
    font-weight: var(--l-fw-bold);
    line-height: var(--l-lh-snug);
  }

  .integration-card:hover .integration-card__title {
    color: var(--l-gold);
  }

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

  /* The endpoint path is emitted, not written, so it is mono -- THE MONO IS
     MEASUREMENT RULE.

     IT WRAPS, AND THE FIRST VERSION SCROLLED. That was two mistakes in one
     declaration. `overflow-x: auto` turns every one of these into a scrollable
     region, and axe caught it immediately: `scrollable-region-focusable`,
     serious, 5 nodes at 1440 and 6 at 390 -- WCAG 2.1.1, because a keyboard user
     cannot reach a scroll container that is not focusable, so the clipped half
     of a 54-character path was unreadable without a mouse. The obvious patch is
     `tabindex="0"`, which is what axe suggests; on this page that would add
     thirty-two tab stops to a reference list, which is a worse page.

     And the justification for scrolling was simply false. I wrote that a wrapped
     path "stops being copy-pasteable" -- wrapping inserts no characters and
     changes nothing about selection or copying. It was a made-up reason for a
     choice that also failed an accessibility gate.

     So it wraps. `overflow-wrap: anywhere` breaks only when a line genuinely
     does not fit, and these paths break after a `/`. No scroll container, no tab
     stops, and the whole path is visible instead of half of it. A card getting
     one line taller is the entire cost. */
  .integration-card__endpoint {
    display: block;
    min-width: 0;
    max-width: 100%;
    margin-block-start: auto;
    padding: var(--space-2);
    background-color: var(--l-ground);
    color: var(--l-label);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-label);
    line-height: var(--l-lh-normal);
    /* `break-word`, NOT `anywhere`. Both wrap a long path, but `anywhere` breaks
       at the first character that does not fit -- so
       `vulnerability-intelligence` split across two lines mid-token, which is
       the version that shipped. `break-word` uses the normal break
       opportunities first, and a `/` is one, so a path now breaks between its
       segments and only falls back to mid-token if a single segment is wider
       than the card. 10 of the 45 paths wrap; this is what decides whether that
       is readable. */
    overflow-wrap: break-word;
    /* Above the stretched link, so a visitor can select and copy the path
       instead of navigating when they click into it. */
    position: relative;
  }

  /* The HTTP method is a neutral machine fact, so it takes the `--plain` stamp
     treatment rather than an accent: gold already means primary action in this
     system and violet already means caution, and a verb is neither. It reads
     first in the row because it is the thing you need before the path. */
  .badge-fh.badge-fh-method {
    color: var(--l-label);
    border-color: var(--l-neatline);
    background-color: transparent;
    font-variant-numeric: normal;
  }

  .integration-card__facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0;
  }

  /* =========================================================================
     0c. THE SIDEBAR NAVIGATION'S STRUCTURE.

     The rail was restructured on 2026-08-31 — the argument, the measurements and
     the IA decisions are in `components/sidebar.html`'s own header comment, which
     is where a reader of the markup will look. This block is only the geometry.

     There is deliberately NO base `.nav-list` rule here. The nav is now real
     `<ul>`/`<li>` for the screen-reader list semantics it was missing, and
     `fh-reset.css` already zeroes list margin and padding and drops the marker
     via `ol[class], ul[class]` — which `.nav-list` matches. Restating it would
     be a second source of truth for the same three properties. Checked, not
     assumed: `p` is in the reset's margin-zeroing set too, so the section
     labels need nothing either.

     What DOES need undoing is `<summary>`: the UA gives it a disclosure triangle
     and `display: list-item`, and both have to go before it can be a nav row.
     ====================================================================== */

  /* The two external jumps and Sign Out. Ruled off from the navigation proper,
     with the hairline doing the separating — this world divides by rule, not by
     a change of ground. Replaces `.sidebar-footer`, which held only Sign Out
     while API Docs floated unruled above the Account group — and which declared
     `margin-top` twice (`auto`, then `1.5rem`), so the `auto` that was meant to
     pin it to the foot of the rail never applied. */
  .sidebar-nav .nav-list--utility {
    margin-block-start: var(--space-3);
    padding-block-start: var(--space-3);
    border-block-start: var(--l-hairline) solid var(--l-neatline);
  }

  /* THE ONE DISCLOSURE, as native `<details>`. No JavaScript, so it survives the
     failure that hid two thirds of this rail: `[x-cloak]` is removed only by
     Alpine, so an Alpine that never loads left every group permanently
     `display: none`. The browser owns the state, the keyboard and the
     announcement, and the server renders `open` straight from the endpoint. */
  .sidebar-nav .nav-group__summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    cursor: pointer;
    /* `list-item` is what draws the triangle; both spellings are needed because
       older WebKit only honours the pseudo-element. */
    list-style: none;
  }

  .sidebar-nav .nav-group__summary::-webkit-details-marker {
    display: none;
  }

  .sidebar-nav .nav-group__summary:hover {
    color: var(--l-ink);
  }

  /* One chevron in the markup, rotated by state, so the open and closed marks
     cannot drift apart the way two separate glyphs can. The old rail swapped
     `bi-chevron-right` for `bi-chevron-down` through an Alpine `:class` binding,
     which meant with JavaScript off it rendered neither. */
  .sidebar-nav .nav-group__chevron {
    transition: rotate var(--l-dur-fast) ease;
  }

  .sidebar-nav .nav-group[open] > .nav-group__summary .nav-group__chevron {
    rotate: 90deg;
  }

  /* The sidebar's group labels. THE LEGIBLE LABEL RULE, twice over: the
     register's weight floor is bold where it titles a column, and ten rules in
     this system have already drifted lighter by inheriting a weight rather than
     declaring one.

     `--l-fs-label` (0.8125rem) is the top of the label band, per the same rule:
     caps plus tracking plus smallness plus lightness is the one combination
     that has to be decoded rather than read, and this register only gets to
     spend three of those four.

     The indent matters as much as the type. At `padding: 0` the label sat at
     x=0 while its own nav items sat at x=24, so the group read as clipped by
     the viewport edge rather than as the title of the items beneath it.

     WHY THIS RULE OUTLIVED THE DEFECT THAT PROMPTED IT. It was written for §0b:
     these labels were `<button>`s, and the reset's `font` shorthand was
     overwriting style.css's declared 700 with an inherited 400. After the
     2026-08-31 restructure none of them is a button any more — two are `<p>`
     and one is a `<summary>` — so that mechanism no longer reaches them. The
     rule stays because a SECOND one does: style.css sets this register to
     `0.75rem`/`700` at `opacity: 0.8` in `@layer legacy`, and those are the
     values The Legible Label Rule and The No Dead Ink Rule reject. It is now an
     override of the incumbent rather than a repair of the reset. Same
     declarations, different argument — recorded because a comment that still
     claimed "the shorthand took it" would send the next reader hunting for a
     button that is not there. */
  .sidebar-nav .nav-section {
    padding: 0.75rem 1.5rem 0.25rem;
    font-size: var(--l-fs-label);
    font-weight: var(--l-fw-bold);
    letter-spacing: var(--l-ls-label);
    color: var(--l-label);
    /* style.css dims this to `opacity: 0.8`, which drops --l-label's measured
       10.7:1 to roughly 8.4:1 and lands it on the floor The No Dead Ink Rule
       reserves for decorative non-text. A group header is the most structural
       text in the rail; if it needed to recede it would be deleted, not
       thinned. Opacity survives the reset untouched, so this is an override,
       not a restoration. */
    opacity: 1;
  }

  /* The API example tabs. The active tab's ONLY state channel was
     `border-bottom-color`, and the reset had already removed the border it
     colours — so `.api-code-tab.active` painted a gold border onto a
     `border-bottom-width: 0`, which renders nothing at all. The selected tab
     was indistinguishable from the other three: four labels in a row, no
     padding between them, no indication of which one the code block belonged
     to. That is why the tab strip read as broken text rather than as tabs.

     The 2px rule is re-declared transparent on the resting tab so selecting one
     changes colour without changing geometry — the strip must not reflow as the
     visitor arrows across it. Colour is not the only channel: the rule's
     presence is the shape, and `aria-selected` carries it to assistive tech. */
  .api-code-tab {
    padding: 0.75rem 1rem;
    border: 0;
    border-block-end: 2px solid transparent;
    font-size: var(--l-fs-chrome);
    font-weight: var(--l-fw-semibold);
    color: var(--l-label);
  }

  .api-code-tab:hover {
    color: var(--l-ink);
    border-block-end-color: var(--l-contour-hi);
  }

  .api-code-tab[aria-selected="true"],
  .api-code-tab.active {
    border-block-end-color: var(--l-gold);
    color: var(--l-gold);
  }

  /* The mobile drawer's open control. It declares an explicit 44px box, so the
     target size survived the reset — but it also declares `font-size: 1.25rem`
     for its glyph, and the shorthand took it. The hamburger has been drawing at
     16px inside a 44px button since the reset landed: a correct target with an
     undersized mark in it.

     `.sidebar-close` is NOT here, and that is a finding rather than an
     oversight. It declares no font-size of its own, so `font: inherit` took
     nothing from it and there is nothing to restore. Adding a size would be
     inventing a value to match a sibling — the reset repair only re-declares
     what the reset actually removed. */
  .sidebar-toggle {
    font-size: 1.25rem;
  }

  /* =========================================================================
     1. APP CHROME vs. THE MARKETING TYPE SCALE.

     landing's fh-base.css sets `h1 { font-size: var(--l-fs-cartouche) }`, which
     clamps up to 4rem/64px. That is right for a page whose h1 IS the page. It
     is wrong for a topbar label that says "Host" beside a search button, and
     because `h1` lives in the `base` layer while `.page-title` lives in
     `legacy`, the marketing size would win: layer order beats specificity, and
     legacy sorts below base.

     So the override lives in `components`, which outranks both. This is the one
     place console deliberately refuses landing's scale, and it is confined to
     the two elements that are furniture rather than content.
     ====================================================================== */
  .topbar .page-title {
    font-size: 1.25rem;
    font-weight: var(--l-fw-bold);
    line-height: var(--l-lh-snug);
    letter-spacing: var(--l-ls-tight);
    margin: 0;
  }

  .topbar-heading {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
  }

  .topbar-trial {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding-block: 0.25rem;
    color: var(--l-label);
    font-size: var(--l-fs-small);
    font-weight: var(--l-fw-regular);
    line-height: var(--l-lh-snug);
    white-space: nowrap;
  }

  /* THE EMPHASIS WAS INVERTED — owner call 2026-08-31, "make it look nicer".

     DESIGN.md pins this component hard: the trial indicator "is account status,
     not a reward or promotion", with "no icon, fill, border, pill, badge shape,
     or accent colour". So nothing is added here. What was actually wrong is
     cheaper than chrome and is a rule this house already owns.

     `Enterprise trial` was ink at semibold and `28 days remaining` was label
     grey — the CATEGORY shouting and the FACT whispering. The No Dead Ink Rule
     settles it in one line: "A record's values are `ink`; `sounding` is for
     running prose." The number is the value. The words around it name the
     context, which is what the label register is for.

     So the count takes ink and the weight, the framing words step back, and the
     separator leaves `--l-land-edge` — an amber-brown that read as a stray warm
     accent in a line that is explicitly not allowed one — for `--l-faint`, this
     world's cool tone for decorative non-text. It is `aria-hidden`, so the
     faint-floor restriction on small text does not reach it.

     Deliberately NOT done: colouring the count when the trial is nearly over.
     Urgency is real information and a reader would want it, but it is an accent
     on the one component whose spec bans accents. Raise it as a spec change or
     not at all. */
  .topbar-trial__label {
    color: var(--l-label);
    font-weight: var(--l-fw-regular);
  }

  .topbar-trial__separator {
    color: var(--l-faint);
  }

  .topbar-trial__remaining {
    color: var(--l-label);
  }

  .topbar-trial__count {
    color: var(--l-ink);
    font-weight: var(--l-fw-semibold);
    /* Tabular so the line does not reflow as the count walks 28 -> 9 -> 1. It
       was on the wrapper before, where the digits it needed to align were not
       the only glyphs in the box. */
    font-variant-numeric: tabular-nums;
  }

  /* Same reasoning for the sidebar: landing's heading scale must not reach it. */
  #sidebar h1,
  #sidebar h2,
  #sidebar h3 {
    font-size: var(--l-fs-data);
  }

  /* ---- 1b. A PANEL TITLE IS NOT A SECTION PLATE.

     The same layer inversion as section 0b, running the other way. `h2` lives
     in `@layer base` at `--l-fs-plate` (clamps to 40px); `.card-title-text`,
     `.org-card-title` and `.fs-h5` live in `@layer legacy`. Base sorts above
     legacy, so the marketing step won and all three classes did nothing.

     Measured at 1440x1000: "Today's activity" and "Credit status" rendered
     40px/800 inside a 360px panel, above data at 14px — a 2.9:1 title-to-body
     ratio INSIDE a card. That is the identical defect DESIGN.md records against
     landing's own h2 ("a shout over a whisper"), and console inherited it by
     copying fh-base.css verbatim, which is exactly what it is supposed to do.
     Five organization result cards printed a company name at the same 40px.

     THE OWNED SCALE RULE ALLOWS THIS AND ONLY THIS. Its wording — "no component
     may re-declare a heading's size" — carries one deliberate exception for a
     named component (`.fh-panel__title`). These are that exception, named, and
     the list is closed: three classes, each already the title element of an
     existing bounded component. A fourth arriving here is drift.

     Each takes back the size its own style.css rule always intended, so this is
     a repair, not a new opinion:
       .card-title-text  1rem/600      (style.css:634)
       .org-card-title   --fs-h3       (style.css:2434)

     `.fs-h5` is the exception to the exception. It reads `var(--fs-h5)`, and
     --fs-h5 IS NOT DEFINED ANYWHERE in this app — not in fullhunt-tokens.css,
     not in the chart layer, not in the bridge. The declaration has always been
     invalid, so the class was dead even before the layer inversion buried it,
     and there is no original intent to restore. Its two call sites are the
     section headings on /billing/upgrade ("Compare individual plans",
     "Frequently Asked Questions"), which are content headings inside an app
     content column, one step below the page head. `--l-fs-d3` (1.375rem/22px)
     is the ramp's own 22px step — the one DESIGN.md names when it records the
     restored 64 / 40 / 34 / 22 sequence. Consuming an existing step keeps this
     off the "declared and unused token" list and out of literal territory. ---- */
  .card-title-text {
    font-size: var(--l-fs-body);
    font-weight: var(--l-fw-semibold);
    letter-spacing: normal;
  }

  .org-card-title {
    font-size: var(--fs-h3);
    font-weight: var(--l-fw-bold);
  }

  .fs-h5 {
    font-size: var(--l-fs-d3);
    font-weight: var(--l-fw-bold);
  }

  /* NAVIGATION IS NOT PROSE.

     landing's `a` rule underlines every link (base layer, gold at 45%). That is
     right on a marketing page, where a link is a phrase inside a sentence and
     the underline is what tells you it is one. It is wrong for app chrome: the
     sidebar's fourteen nav items and the topbar's actions are a MENU, where
     position, icon and highlight already carry the affordance, and underlining
     all of them turns the rail into a wall of ruled text.

     Scoped tightly to the two chrome regions, so a genuine inline link in page
     content keeps landing's treatment. `text-decoration-line`, not the
     `text-decoration` shorthand, so the thickness/offset/colour landing set are
     left intact for the hover state below to use. */
  #sidebar a,
  .topbar a,
  .topbar .btn {
    text-decoration-line: none;
  }

  /* A CONTROL IS NOT A LINK, whatever element it is built from. Bootstrap's
     `.btn` is routinely applied to an <a>, which landing's base rule then
     underlines -- so "Continue with Google" shipped as an underlined button
     face. A button already reads as clickable through its own box; the
     underline is the affordance a link needs precisely BECAUSE it has no box.
     Applies app-wide, unlike the nav exception above. */
  a.btn,
  a.fh-btn {
    text-decoration-line: none;
  }

  /* NO UNDERLINE ON HOVER EITHER — owner call 2026-08-31.

     This rule used to re-add the line on hover, and the reasoning above it
     ("NAVIGATION IS NOT PROSE") is exactly why it should not have. Underlining
     all fourteen nav items at rest was rejected for turning the rail into a wall
     of ruled text; doing it one item at a time on hover is the same mistake at
     1/14th scale, and it is the one the pointer is looking at.

     The affordance does not need it. `.nav-link:hover` already changes the row's
     background AND turns its icon gold — two channels, both measured — so the
     underline was a third signal on an element that is unambiguously a menu row
     to begin with. Chrome is a menu at rest and on hover.

     `text-decoration-line` on both states, not the shorthand, so landing's
     thickness, offset and colour survive for any genuine inline link that ends
     up inside these regions. */
  #sidebar a:hover,
  .topbar a:hover {
    text-decoration-line: none;
  }

  /* THE DASHBOARD'S QUICK ACTION ROWS. Same layer inversion as everything else
     in this file: style.css declares `text-decoration: none` on
     `.quick-action-link` in `@layer legacy`, and landing's `a { text-decoration:
     underline }` sits in `@layer base` — base outranks legacy, so all seven rows
     shipped underlined, and the line propagated from the anchor down onto both
     the title and the description. Measured: `underline` at rest AND on hover.

     A quick action is a menu row, not a reference in a sentence — the same
     argument as the sidebar above. The row already carries an icon, a bordered
     bottom rule, a hover background and a trailing chevron; it does not need a
     fifth affordance, and the underline was landing on text that reads as a
     heading.

     `text-decoration-line` only, so the row's own hover treatment is untouched. */
  .quick-action-link,
  .quick-action-link:hover {
    text-decoration-line: none;
  }

  /* =========================================================================
     2. THE INCUMBENT CHIP FAMILIES, brought onto the system centrally.

     `.badge-fh` has 97 call sites across 24 templates and `.tag-badge` 3, so
     these are restyled HERE rather than template by template -- one edit moves
     every unmigrated readout in the app.

     Two defects in the legacy rules are fixed by being outranked rather than
     edited:

     * `.badge-fh` is declared TWICE in style.css (~934 and ~3058), which is its
       own tell -- nobody knew the first one was there.
     * A media query shrank it to `font-size: 0.65rem` (10.4px) on small
       screens. Below the label register's floor, and combined with uppercase
       and tracking it is the exact unreadable combination The Legible Label
       Rule names. A layered rule beats a legacy rule inside a media query, so
       this is fixed without touching the media query.
     ====================================================================== */
  .badge-fh {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.25rem 0.5rem;
    border: var(--l-hairline) solid var(--l-neatline);
    border-radius: var(--l-radius-sm);
    font-family: var(--l-face-display);
    font-size: var(--l-fs-label);
    /* DECLARED, never inherited: ten of landing's nineteen label rules silently
       inherited 400 because they never set a weight. */
    font-weight: var(--l-fw-semibold);
    line-height: 1.35;
    white-space: nowrap;
  }

  /* A tag is a FILTERABLE MACHINE FACT -- clicking one runs `tag:<value>`
     against the corpus -- so it takes the value treatment: cyan, mono, filled.
     It shipped as a green pill, which spent --fh-success on a non-severity
     meaning and made a row of nine tags read as nine passing checks. */
  .tag-badge {
    display: inline-flex;
    align-items: center;
    padding: 0.25rem 0.5rem;
    border: var(--l-hairline) solid rgb(var(--l-accent-2-rgb) / 0.32);
    border-radius: var(--l-radius-sm);
    background: rgb(var(--l-accent-2-rgb) / 0.1);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-data);
    line-height: 1.35;
    white-space: nowrap;
  }

  .tag-badge,
  .tag-badge a { color: var(--l-accent-2); text-decoration: none; }

  .tag-badge:hover { background: rgb(var(--l-accent-2-rgb) / 0.18); }

  /* A DNS RECORD VALUE IS A MACHINE FACT, so it takes the same value-chip
     treatment as every other filterable value in this app.

     It shipped as `#d63384` magenta on a 10% magenta fill -- a hue that belongs
     to no FullHunt palette (it is Bootstrap 4's pink, left over from an earlier
     theme) and one that axe reports as a contrast failure on the IP-lookup
     page. It is also the last visible instance of the magenta that the host
     record used to print its DNS labels in. */
  .dns-badge {
    background: rgb(var(--l-accent-2-rgb) / 0.1);
    border: var(--l-hairline) solid rgb(var(--l-accent-2-rgb) / 0.32);
    color: var(--l-accent-2);
    font-family: var(--l-face-data);
  }

  /* CLASSIFICATION, not state. Cloud / CDN / IPv6 / Private IP describe what
     KIND of thing a host is; nothing is wrong when they are present. They
     shipped in four different hues, which said they were four different kinds
     of thing. A bordered neutral chip is the house stamp for a classification,
     and with them quiet the two chips that DO carry state are the only coloured
     things in the row. */
  .badge-fh.badge-info,
  .badge-fh.badge-primary,
  .badge-fh.badge-purple {
    background: transparent;
    border-color: var(--l-neatline);
    color: var(--l-label);
  }

  /* STATE. Takes landing's `.hd-badge` measurement so the same fact looks the
     same in both apps -- console shipped LIVE green while landing shipped it
     red, which is the two apps disagreeing about what one fact looks like. */
  .badge-fh.badge-danger {
    background: rgb(var(--l-critical-rgb) / 0.22);
    border-color: transparent;
    color: var(--fh-danger);
    font-weight: var(--l-fw-bold);
    letter-spacing: var(--l-ls-label);
    text-transform: uppercase;
  }

  /* =========================================================================
     3b. SLOP THE BRIDGE COULD NOT REACH.

     Re-pointing the tokens moved the app onto the chart's ground, ink and
     borders. It could not fix rules that hardcode a GRADIENT or spend a
     SEVERITY colour on something that is not a severity -- those survive any
     palette change, because the palette is not where they live.
     ====================================================================== */

  /* GRADIENTS CLIPPED TO TEXT ARE BANNED -- a colour ramp behind the glyphs
     with a transparent text fill, applied to the "Organization Database" page
     title, the sidebar's company name, the auth logo fallback and a globe glyph
     on the host page.

     DESIGN.md is explicit twice over: emphasis comes from weight or size, and
     "the wordmark's amber->orange gradient stays RESERVED FOR THE WORDMARK".
     A gold-to-purple wash across a heading is also the two brand accents doing
     each other's jobs, which The Two Jobs Rule exists to prevent.

     Not deleted from style.css -- these two classes have call sites across
     several unmigrated templates and the markup is fine; only the paint was
     wrong. Overriding here neutralises every one of them at once. */
  .text-gradient,
  .text-gradient-2 {
    background: none;
    background-clip: border-box;
    -webkit-background-clip: border-box;
    -webkit-text-fill-color: currentColor;
    color: var(--l-gold);
  }

  /* The org-database page title carried its own copy of the same trick --
     `linear-gradient(135deg, #fff, var(--fh-cyan))` clipped to the text, so a
     47px heading faded from white into cyan. It is not a `.text-gradient` call
     site, which is why neutralising that class did not reach it, and it is the
     accent doing display duty on a page that is not a readout. Ink, at the
     app's own display step. */
  .console-search-hero-title {
    background: none;
    background-clip: border-box;
    -webkit-background-clip: border-box;
    -webkit-text-fill-color: currentColor;
    color: var(--l-ink);
    font-size: var(--l-fs-page-head);
    letter-spacing: var(--l-ls-tight);
  }

  /* ANCHOR THE ENTRY TO THE TOP. `.console-search-hero` is a flex column with
     `min-height: calc(100vh - 64px)` -- measured 900px -- which pushed the field
     into the vertical middle of an otherwise empty screen. DESIGN.md's Search
     Entry Exception is explicit that content sits at the TOP: "a field pinned to
     the exact middle reads as floating", which is exactly how it read. The
     ported attack-surface entry already behaves this way; this makes the org
     database's entry match it. */
  .console-search-hero {
    min-height: 0;
    justify-content: flex-start;
    padding-block: clamp(1.5rem, 6vh, 4rem) var(--l-space-12);
  }

  /* Shared search-engine entry. Attack Surface Search established this
     composition; IP and organization lookup use the same hierarchy and
     interaction language instead of separate legacy heroes. */
  .sr-entry {
    padding-block: 0;
  }

  .sr-hero {
    position: relative;
    isolation: isolate;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: clamp(30rem, calc(100svh - 5rem), 52rem);
    padding-block: 3.25rem 4rem;
    text-align: center;
  }

  .sr-hero h1 {
    margin: 1.25rem 0 2rem;
    color: var(--l-ink);
    font-size: var(--l-fs-d1);
    font-weight: var(--l-fw-bold);
    letter-spacing: -0.01em;
    text-wrap: balance;
  }

  .sr-hero__mark {
    width: min(22rem, 78%);
    height: auto;
  }

  .sr-hero .fh-entry {
    margin-inline: auto;
  }

  .sr-hero .fh-entry__hint {
    justify-content: center;
    margin-block: 0.75rem 0;
    opacity: 0.7;
  }

  .sr-hero .fh-entry__submit {
    white-space: nowrap;
  }

  /* The organization combobox is part of the position-entry control. Keep its
     overlay in the same flat chart material; legacy button rules otherwise
     outrank the keyboard-active tint at the component layer. */
  .sr-hero .suggestions-list {
    background: var(--l-deep);
    border: var(--l-hairline) solid var(--l-contour-hi);
    border-radius: var(--l-radius-sm);
    box-shadow: none;
  }

  .sr-hero .suggestions-list .suggestion-item {
    border-inline: 0;
    border-block-start: 0;
    border-block-end: var(--l-hairline) solid var(--l-neatline);
    background-color: transparent;
    color: var(--l-sounding);
  }

  .sr-hero .suggestions-list .suggestion-item:last-child {
    border-block-end: 0;
  }

  .sr-hero .suggestions-list .suggestion-item:hover,
  .sr-hero .suggestions-list .suggestion-item.is-active {
    background-color: rgb(var(--l-gold-rgb) / 0.08);
    color: var(--l-ink);
  }

  .sr-hero__example {
    color: var(--l-gold);
    overflow-wrap: anywhere;
  }

  .sr-hero__lead {
    max-width: 32rem;
    margin: 2.75rem 0 0;
    color: var(--l-label);
    font-size: var(--l-fs-body);
    line-height: var(--l-lh-relaxed);
    text-wrap: balance;
  }

  .sr-hero__scope {
    display: block;
    width: max-content;
    max-width: 100%;
    margin: 1.5rem 0 0;
  }

  .sr-hero__scope .fh-symbol {
    display: inline-block;
    margin-block-end: 0.125rem;
    vertical-align: middle;
  }

  .sr-tries {
    width: min(42rem, 100%);
    margin-block-start: 2.5rem;
    text-align: start;
  }

  .sr-tries__list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0.625rem;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .sr-tries__row {
    display: grid;
  }

  .sr-try {
    display: grid;
    grid-template-rows: 1fr auto;
    gap: 0.5rem;
    padding: 0.9375rem 1rem;
    border-radius: var(--l-radius-card);
    text-decoration: none;
    transition: border-color var(--l-dur-fast) var(--l-ease);
  }

  .sr-try::after {
    border-radius: inherit;
  }

  .sr-try__say {
    color: var(--l-ink);
    font-family: var(--l-face-display);
    font-size: var(--l-fs-small);
    line-height: var(--l-lh-normal);
  }

  .sr-try__run {
    display: inline-flex;
    align-items: center;
    gap: 0.4375rem;
    min-width: 0;
  }

  .sr-try__q {
    color: var(--l-gold);
    border-block-end: var(--l-hairline) dashed rgb(var(--l-gold-rgb) / 0.5);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-label);
    line-height: var(--l-lh-normal);
    /* `break-word`, NOT `anywhere`. Both wrap a long path, but `anywhere` breaks
       at the first character that does not fit -- so
       `vulnerability-intelligence` split across two lines mid-token, which is
       the version that shipped. `break-word` uses the normal break
       opportunities first, and a `/` is one, so a path now breaks between its
       segments and only falls back to mid-token if a single segment is wider
       than the card. 10 of the 45 paths wrap; this is what decides whether that
       is readable. */
    overflow-wrap: break-word;
  }

  .sr-try__go {
    color: var(--l-gold);
    opacity: 0.5;
    transition: opacity var(--l-dur-fast) var(--l-ease);
  }

  .sr-try:hover,
  .sr-try:focus-visible {
    border-color: var(--l-gold);
  }

  .sr-try:hover .sr-try__q,
  .sr-try:focus-visible .sr-try__q {
    border-block-end-style: solid;
  }

  .sr-try:hover .sr-try__go,
  .sr-try:focus-visible .sr-try__go {
    opacity: 1;
  }

  .sr-hero::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image:
      radial-gradient(62% 44% at 50% 16%, rgb(var(--l-gold-rgb) / 0.07), transparent 70%),
      linear-gradient(rgb(var(--l-sounding-rgb) / 0.022) var(--l-hairline), transparent var(--l-hairline)),
      linear-gradient(90deg, rgb(var(--l-sounding-rgb) / 0.022) var(--l-hairline), transparent var(--l-hairline));
    background-size: 100% 100%, 60px 60px, 60px 60px;
    -webkit-mask-image: radial-gradient(ellipse at 50% 38%, #000 20%, transparent 72%);
    mask-image: radial-gradient(ellipse at 50% 38%, #000 20%, transparent 72%);
  }

  @media (min-width: 40rem) {
    .sr-tries__list {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  /* A FEATURE IS NOT A PASSING CHECK. The plan cards drew 24 green
     `check-circle-fill` glyphs down the pricing table. Green is
     `--fh-success`, a value from the severity family, and spending it on "this
     plan includes X" teaches a reader that green means nothing in particular --
     which is exactly the reading that makes a real green status unreadable
     later. It is the same mistake as the LIVE badge, at 24x.

     Gold: this is the brand's affirmative mark, and the list is a list of
     things you get. */
  .pricing-card .plan-features li i,
  .pricing-card .plan-features li .bi {
    color: var(--l-gold);
  }

  /* The executable-example row under a search entry. The house mark for a
     runnable example query is the dashed underline, which is what tells a
     reader these three words are controls and not a caption. */
  .cs-orgtries {
    margin-block: var(--space-3) 0;
    text-align: start;
    font-size: var(--l-fs-small);
    color: var(--l-label);
  }

  .cs-orgtries__label { margin-inline-end: 0.35em; }

  .cs-orgtries a {
    margin-inline-end: 0.75em;
    color: var(--l-gold);
    font-family: var(--l-face-data);
    font-size: var(--l-fs-data);
    text-decoration: underline dashed;
    text-decoration-color: rgb(var(--l-gold-rgb) / 0.45);
    text-underline-offset: 0.3em;
  }

  .cs-orgtries a:hover { text-decoration-color: var(--l-gold); }

  /* A CREDIT BALANCE IS NOT A SUCCESS STATE. Five pages printed it as
     `badge-success` -- --fh-success green, from the severity family -- in the
     topbar, next to chips that use green to mean "passing". A balance is a
     quantity you hold, and it goes gold like every other quantity in this brand.
     The number itself is mono, because a machine produced it. */
  .badge-fh.badge-fh-credits {
    background: rgb(var(--l-gold-rgb) / 0.12);
    border-color: rgb(var(--l-gold-rgb) / 0.35);
    color: var(--l-gold);
  }

  /* The password-strength track. Was an inline `style` carrying its height and
     a white alpha wash; now a class, on the chart's own rule colour. */
  .progress.fh-strength {
    height: 4px;
    background: rgb(var(--l-neatline-rgb) / 0.55);
  }

  /* The credit meter at its healthy end. A balance is a quantity, so it is
     gold; the low and critical states keep amber and red, because those are
     genuinely warnings. */
  .progress-bar.fh-meter {
    background-color: var(--l-gold);
  }

  /* `.btn-fh-secondary` painted its label in `--fh-cyan` on a `--fh-bg-tertiary`
     fill with three `!important`s. On the promo form that made the only submit
     button on the panel read as a bare cyan text link rather than a control, and
     it spends the readout accent on app chrome -- --l-accent-2 is scoped to
     search and host, and never carries an action.

     `!important` here only because the rule it corrects is itself `!important`;
     it goes away with those three. */
  .btn-fh-secondary {
    color: var(--l-ink) !important;
    border-color: var(--l-contour-hi) !important;
  }

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

  /* Plan badges. `.plan-enterprise` was Bootstrap blue (#0060ff at 15%), which
     is in no FullHunt palette, and the enterprise tier is not a state. All
     three tiers now read as one KIND of thing -- a plan -- distinguished by
     their text, not by three unrelated hues. */
  /* Matched on the tier class ALONE, not on `.user-plan`. The sidebar uses
     `.user-plan.plan-enterprise` but the settings page uses
     `.badge-fh.plan-enterprise`, so a `.user-plan`-scoped rule left the
     "Enterprise Account" badge Bootstrap-blue on that one page. */
  .plan-enterprise,
  .plan-professional,
  .plan-consultant {
    background: rgb(var(--l-gold-rgb) / 0.12);
    color: var(--l-gold);
  }

  .role-guide {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-3);
  }

  .role-guide > span {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: var(--space-2);
    line-height: var(--l-lh-relaxed);
  }

  /* Organization classifications are external data and can be far longer
     than an interface chip. Keep the stamp treatment without forcing the page
     wider than the viewport. */
  .host-hero .badge-fh {
    max-width: 100%;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  /* =========================================================================
     3. SHELL GAPS.
     ====================================================================== */

  /* THE TOPBAR COULD NOT SHRINK, AND IT WAS THE WHOLE MOBILE STORY.

     Measured at 360px on the live app, loading fresh at that width: seven pages
     scrolled horizontally, and on six of them `.topbar-actions` accounted for
     the overflow TO THE PIXEL -- audit +140 against an actions group whose
     right edge sat at 499, oem +127 at 487, org-search +148 at 508, countries
     +156 at 513, ip-lookup +93 at 453, orgs +74 at 434.

     The cause is two flex defaults compounding. `.topbar` is
     `justify-content: space-between` with `flex-wrap: nowrap`, and a flex item's
     `min-width` is `auto` -- so neither the title group nor the actions group
     can shrink below its content, and with nowrap there is nowhere for the
     surplus to go. The page title, a Search button, a credits chip and an avatar
     simply do not fit in 360px on one line, and never could.

     `min-width: 0` restores the ability to shrink; wrapping gives the surplus
     somewhere to go. Both groups keep their order, so nothing moves on desktop.

     NOT fixed by hiding the actions at mobile: they are the credits balance and
     the account menu, which is exactly what a user checks on a phone. */
  .topbar { gap: var(--space-3); }

  .topbar > * {
    min-width: 0;
  }

  .topbar-actions {
    flex-wrap: wrap;
    justify-content: flex-end;
  }

  @media (max-width: 767.98px) {
    .topbar {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: center;
      row-gap: var(--space-2);
    }

    .topbar-heading {
      grid-column: 1;
      grid-row: 1;
    }

    .topbar-actions {
      grid-column: 2;
      grid-row: 1;
      flex-wrap: nowrap;
    }

    .topbar-trial {
      grid-column: 1 / -1;
      grid-row: 2;
      justify-self: start;
    }

    .topbar .page-title {
      font-size: 1rem;
      overflow-wrap: anywhere;
    }
  }

  @media (max-width: 359.98px) {
    .topbar-trial__remaining {
      white-space: normal;
    }
  }

  /* Data tables become labelled records on narrow screens. Horizontal scroll
     hid the right-hand fields with no visible affordance, which made audit and
     OEM logs look truncated and made member actions effectively unreachable. */
  @media (max-width: 767.98px) {
    .table-responsive.fh-table-cards {
      overflow: visible;
    }

    .fh-table-cards table,
    .fh-table-cards tbody,
    .fh-table-cards tr,
    .fh-table-cards td {
      display: block;
      width: 100%;
    }

    .fh-table-cards thead {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }

    .fh-table-cards tbody tr {
      padding-block: var(--space-2);
    }

    .fh-table-cards tbody tr + tr {
      border-block-start: var(--l-hairline) solid var(--l-neatline);
    }

    .fh-table-cards tbody td {
      display: grid;
      grid-template-columns: minmax(5.75rem, 32%) minmax(0, 1fr);
      align-items: start;
      gap: var(--space-3);
      max-width: none;
      padding: 0.625rem 1rem;
      border: 0;
      white-space: normal;
      overflow-wrap: anywhere;
    }

    .fh-table-cards tbody td::before {
      content: attr(data-label);
      color: var(--l-label);
      font-size: var(--l-fs-label);
      font-weight: var(--l-fw-bold);
      letter-spacing: var(--l-ls-label);
      line-height: var(--l-lh-snug);
      text-transform: uppercase;
    }

    .fh-table-cards tbody td > * {
      min-width: 0;
      max-width: 100%;
    }

    .fh-table-cards .badge-fh {
      max-width: 100%;
      white-space: normal;
      overflow-wrap: anywhere;
    }

    .role-guide {
      grid-template-columns: minmax(0, 1fr);
    }
  }

  /* THE BOOTSTRAP ROW'S NEGATIVE GUTTER HAD NOTHING TO SIT IN.

     `.row` carries `margin-inline: -0.75rem` (-12px) and relies on an ancestor's
     padding to absorb it. `.content-area` drops to `padding: 0.5rem` (8px) below
     576px, which is 4px short on each side -- so every page whose content is a
     Bootstrap row was exactly 8px wider than the viewport. Measured: api-keys
     +4, and it compounds with the topbar on ip-lookup.

     0.75rem is not a taste choice; it is the smallest value that covers the
     gutter it has to absorb. */
  @media (max-width: 575.98px) {
    .content-area {
      padding: 0.75rem;
    }
  }

  /* A full-bleed element's negative margin must equal the padding it is
     cancelling, or it bleeds past the viewport. `.console-search-hero` hardcodes
     `-1rem` to escape `.content-area`, which no longer matches now that the
     mobile padding is 0.75rem -- 4px too far on each side. Same class of bug as
     the `50vw` bleed on the ported readouts: a bleed written against one
     container's geometry, used inside another. */
  @media (max-width: 767.98px) {
    .console-search-hero {
      margin-inline: -0.75rem;
    }

    /* The organization profile reuses the legacy full-bleed host header.
       Its -1.5rem mobile margin was written for 1.5rem content padding, while
       the console shell uses 0.75rem below this breakpoint. Match the actual
       shell gutter so the header reaches the edge without widening the page. */
    .host-hero {
      margin-inline: -0.75rem;
    }
  }

  /* The auth card's heading. These pages had NO heading at all -- the card
     opened straight into the form -- so every one failed
     `page-has-heading-one`. Sized off the app's own step, not landing's
     cartouche clamp: this is a 400px card, not a page-wide hero. */
  .auth-title {
    margin: 0 0 var(--space-6);
    /* FIXED, and NOT --l-fs-d1. Since landing's token file was copied in
       wholesale, --l-fs-d1 is landing's display step -- clamp(2rem, 4.2vw,
       3.25rem), which rendered "Sign in" at ~50px inside a 400px card and left
       the field label crowding under its descenders. An auth card is furniture,
       not a hero: it gets one fixed size off the app's own scale. */
    font-size: 1.5rem;
    line-height: var(--l-lh-snug);
    font-weight: var(--l-fw-bold);
    letter-spacing: var(--l-ls-tight);
    color: var(--l-ink);
  }

  /* The error pages' heading now wraps BOTH the status code and its label in a
     single <h1>, so the accessible name reads "404 Signal Lost" rather than
     either half alone. `.error-code` and `.error-label` were block-level divs
     and are now spans inside that h1, so they have to be re-blocked to keep the
     stacked layout they already had -- nothing moves visually. */
  .error-heading {
    margin: 0;
    font-size: inherit;
    font-weight: inherit;
    letter-spacing: normal;
  }

  .error-heading .error-code,
  .error-heading .error-label {
    display: block;
  }

  /* The one site-wide notice region. Deliberately NOT `.content-area`, whose
     `flex: 1` made an EMPTY notice region grow instead of collapse -- 153px of
     dead space on every page in the app, measured at 1440x900. */
  .fh-notices {
    flex: 0 0 auto;
    padding: var(--l-space-8) var(--l-space-8) 0;
  }

  /* The skip target must not draw a ring when it takes programmatic focus from
     the skip link: it is a landmark, not a control. */
  main:focus { outline: none; }
}

@layer base {
  /* A sticky topbar breaks every in-page anchor -- a fragment jump lands its
     target underneath the bar. Landing learned this expensively (playbook
     mistake 19: eleven anchor targets landing fully hidden, and no gate catches
     it, because the page is correct and only the scroll position is wrong).
     Fixed once here rather than per element. */
  :target { scroll-margin-block-start: 5rem; }

  /* Browser surfaces. The parts nobody draws still carry the design. Console
     already themed its scrollbar; selection and the caret were still the user
     agent's. */
  ::selection { background: rgb(var(--l-gold-rgb) / 0.28); color: var(--l-ink); }
  input, textarea { caret-color: var(--l-gold); }
}

@layer utilities {
  /* Alpine's pre-init guard. This was an inline <style> in the shell's <head>,
     the one place a rule can outrank every layer in the app by accident. It is
     a genuine `!important` -- it must beat whatever `display` the cloaked
     element's own rules give it -- so it lives in `utilities`, which is where
     this system allows one. */
  [x-cloak] { display: none !important; }
}
