/* ==========================================================================
   ndxer design system -- Orange & Graphite (owner-approved 2026-09-24).
   Supersedes the original Paper & Lime palette and oversized display type.
   See docs/superpowers/ledgers/2026-09-24-orange-graphite-design.md.
   Tokens remain the shared source for Bootstrap, forms, and /top/ charts.
   Bright accent fills use dark text; deep orange is used for text on paper.
   Light theme only. No build step, external fonts, or extra framework.
   ========================================================================== */

/* Self-hosted variable fonts (ndxer/static/fonts/README.md -- source URLs,
   versions, licences, and the subsetting command that produced them).
   font-display: swap -- the fallback stack in --ndx-sans/--ndx-mono is
   metric-similar enough that the swap is not a layout jolt, and a blocked
   font must never blank the page (UX §8.4).
   Deleting or renaming one of these files without editing the matching
   url() breaks `collectstatic` in production, not just the look: whitenoise's
   CompressedManifestStaticFilesStorage resolves every url() here. */
@font-face {
  font-family: "Space Grotesk";
  src: url("../fonts/space-grotesk-var-latin.e479274125a5.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Inter";
  src: url("../fonts/inter-var-latin.eb083991cee7.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/jetbrains-mono-var-latin.9b077b9e9749.woff2") format("woff2-variations");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   Tokens (UX §5.1)
   ========================================================================== */

:root {
  /* ---- surfaces & ink ---- */
  --ndx-paper: #f0efeb;
  --ndx-surface: #ffffff;
  --ndx-sunken: #e3e2de;
  --ndx-ink: #161616;
  --ndx-ink-muted: #62605c;
  --ndx-ink-onink: #ecebe7;
  --ndx-border: #d4d2cc;
  --ndx-border-strong: #827e76;

  /* Orange fills, darker orange for readable text and focus on paper. */
  --ndx-accent: #e74722;
  --ndx-accent-hover: #ef5b38;
  --ndx-accent-press: #e74722;
  --ndx-accent-ink: #161616;
  --ndx-accent-soft: #fbe4dc;
  --ndx-accent-deep: #a83215;
  --ndx-select-bg: #f9c8b8;

  /* ---- semantics. The accent never signals state; these do. ---- */
  --ndx-success: #106b2e;
  --ndx-success-soft: #e9f6ec;
  --ndx-warning: #8a5a00;
  --ndx-warning-soft: #fff3cf;
  --ndx-danger: #c0182a;
  --ndx-danger-soft: #fce9e7;
  --ndx-info: #0b5fd6;
  --ndx-info-soft: #e7f1fe;
  /* Class colours (owner-locked 2026-10-10,
     docs/superpowers/specs/2026-10-10-row-actions-and-class-colours.md).
     Okabe-Ito hues so the four classes stay apart under the common colour
     vision deficiencies; -text is the darker label colour that keeps 4.5:1
     on -soft. One meaning per colour everywhere: tabs, pills, menus, toasts,
     facets and the graph. The brand accent is never a class colour. */
  --ndx-class-subject: #009e73;
  --ndx-class-subject-text: #007a58;
  --ndx-class-subject-soft: #d9f2ea;
  --ndx-class-review: #e69f00;
  --ndx-class-review-text: #8a5a00;
  --ndx-class-review-soft: #fcefd2;
  --ndx-class-infrastructure: #0072b2;
  --ndx-class-infrastructure-text: #005a8c;
  --ndx-class-infrastructure-soft: #dceaf5;
  --ndx-class-dismissed: #9a9a9a;
  --ndx-class-dismissed-text: #555555;
  --ndx-class-dismissed-soft: #ebebeb;

  /* ---- type ---- */
  --ndx-sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --ndx-display: "Space Grotesk", var(--ndx-sans);
  --ndx-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  /* Compact display type approved 2026-09-24; data remains the focus. */
  --ndx-fs-display: clamp(2rem, 1.6rem + 1vw, 2.5rem);
  --ndx-fs-h1: 2rem;
  --ndx-fs-h2: 1.75rem;
  --ndx-fs-h3: 1.375rem;
  --ndx-fs-h4: 1.0625rem;
  --ndx-fs-eyebrow: 0.75rem;
  --ndx-fs-lead: 1rem;
  --ndx-fs-body: 1rem;
  --ndx-fs-small: 0.875rem;
  --ndx-fs-table: 0.9rem;
  --ndx-fs-th: 0.75rem;
  --ndx-fs-code: 0.875rem;
  --ndx-fs-meta: 0.8125rem;

  /* One measure for all running prose (68ch ~ 600-650px at body size).
     Before this token, .lead had 62ch, the footer 56ch, and .ndx-section
     paragraphs nothing at all, so landing copy ran ~170 characters wide. */
  --ndx-measure: 68ch;

  /* ---- shape. Tight radii read as instrument, not as app-store icon. ---- */
  --ndx-radius-sm: 3px;
  --ndx-radius: 4px;
  --ndx-radius-lg: 6px;

  /* The only shadow in the system, and only for layers that float
     (dropdown, modal, offcanvas). Cards never get one. */
  --ndx-shadow-pop: 0 1px 2px rgb(18 22 27 / 6%), 0 8px 24px rgb(18 22 27 / 10%);

  /* ================= Bootstrap 5.3 overrides ================= */
  --bs-body-bg: var(--ndx-paper);
  --bs-body-color: var(--ndx-ink);
  --bs-body-font-family: var(--ndx-sans);
  --bs-body-font-size: var(--ndx-fs-body);
  --bs-body-line-height: 1.6;
  --bs-font-monospace: var(--ndx-mono);
  --bs-emphasis-color: var(--ndx-ink);
  --bs-heading-color: var(--ndx-ink);
  --bs-secondary-color: var(--ndx-ink-muted);
  --bs-secondary-bg: var(--ndx-sunken);
  --bs-tertiary-bg: var(--ndx-sunken);
  --bs-tertiary-color: var(--ndx-ink-muted);
  --bs-border-color: var(--ndx-border);
  --bs-border-color-translucent: var(--ndx-border);
  --bs-border-radius: var(--ndx-radius);
  --bs-border-radius-sm: var(--ndx-radius-sm);
  --bs-border-radius-lg: var(--ndx-radius-lg);
  --bs-border-radius-xl: var(--ndx-radius-lg);
  --bs-code-color: var(--ndx-ink);
  --bs-highlight-bg: var(--ndx-accent-soft);

  /* The --bs-*-rgb triples must stay literal: Bootstrap feeds them to
     rgba(), which cannot parse a hex, so they are the one place a colour
     is written twice. Keep each triple in sync with its --ndx-* token. */
  --bs-primary: var(--ndx-accent);
  --bs-primary-rgb: 231, 71, 34;
  --bs-primary-text-emphasis: var(--ndx-accent-deep);
  --bs-primary-bg-subtle: var(--ndx-accent-soft);
  --bs-primary-border-subtle: var(--ndx-accent-press);

  /* Links resolve to ink; the underline (§4.6) carries the affordance. */
  --bs-link-color: var(--ndx-ink);
  --bs-link-color-rgb: 18, 22, 27;
  --bs-link-hover-color: var(--ndx-ink);
  --bs-link-hover-color-rgb: 18, 22, 27;
  --bs-success: var(--ndx-success);
  --bs-success-rgb: 16, 107, 46;
  --bs-success-bg-subtle: var(--ndx-success-soft);
  --bs-success-text-emphasis: var(--ndx-success);
  --bs-warning: var(--ndx-warning);
  --bs-warning-rgb: 138, 90, 0;
  --bs-warning-bg-subtle: var(--ndx-warning-soft);
  --bs-warning-text-emphasis: var(--ndx-warning);
  --bs-danger: var(--ndx-danger);
  --bs-danger-rgb: 192, 24, 42;
  --bs-danger-bg-subtle: var(--ndx-danger-soft);
  --bs-danger-text-emphasis: var(--ndx-danger);
  --bs-info: var(--ndx-info);
  --bs-info-rgb: 11, 95, 214;
  --bs-info-bg-subtle: var(--ndx-info-soft);
  --bs-info-text-emphasis: var(--ndx-info);
  --bs-focus-ring-width: 2px;
  --bs-focus-ring-color: var(--ndx-accent-soft);
}

/* ==========================================================================
   Base type (UX §5.2)
   ========================================================================== */

body {
  font-size: var(--ndx-fs-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Element selectors and Bootstrap's .h1-.h6 utility classes share one
   rule set. Bootstrap ships .h1-.h6 with their own (vw-clamped) font-size,
   and a class selector beats an element selector no matter which
   stylesheet loads second; every /app/* page writes `<h1 class="h2">` to get
   a smaller page title (design system §6.7 "Dashboard <- h1, 1.5rem (not
   display)"), and would otherwise escape this type scale entirely. Listing
   the classes here means the class picks a SIZE, never a different scale. */
h1,
h2,
h3,
.h1,
.h2,
.h3 {
  font-family: var(--ndx-display);
  font-weight: 600;
}

h4,
h5,
h6,
.h4,
.h5,
.h6 {
  font-family: var(--ndx-sans);
  font-weight: 600;
}

h1,
.h1 {
  font-size: var(--ndx-fs-h1);
  line-height: 1.15;
  letter-spacing: -0.02em;
}

h2,
.h2 {
  font-size: var(--ndx-fs-h2);
  line-height: 1.2;
  letter-spacing: -0.015em;
}

h3,
.h3 {
  font-size: var(--ndx-fs-h3);
  line-height: 1.3;
  letter-spacing: -0.01em;
}

h4,
.h4,
.h5 {
  font-size: var(--ndx-fs-h4);
  line-height: 1.35;
}

.h6 {
  font-size: var(--ndx-fs-small);
  line-height: 1.4;
}

/* Hero only. Bootstrap's .display-6 is remapped rather than a new class so
   core/home.html's existing markup keeps working. */
.display-6,
.ndx-display {
  font-family: var(--ndx-display);
  font-size: var(--ndx-fs-display);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.035em;
  text-wrap: balance;
}

.lead {
  font-size: var(--ndx-fs-lead);
  line-height: 1.55;
  font-weight: 400;
  color: var(--ndx-ink-muted);
}

/* The measure, applied once. `main` carries .ndx-prose (base.html), so
   every paragraph under it is capped; wide layouts (tables, cards, the
   plan grid) are unaffected because only <p> and the FAQ body are listed. */
.ndx-prose p,
.ndx-prose .accordion-body,
.ndx-prose .lead,
.ndx-band p,
.ndx-footer p {
  max-inline-size: var(--ndx-measure);
}

/* Uppercase mono section label. Replaces a small heading, never replaces
   the real <h2> -- put it in a <p> above the heading or inside the h2 as a
   <span>, so the document outline stays correct for screen readers. */
.ndx-eyebrow {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-eyebrow);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ndx-ink-muted);
}

code,
kbd,
pre,
samp {
  font-family: var(--ndx-mono);
}

/* Inline <code> is a chip, not a syntax fragment: monospace + a faint
   sunken plate, never its own hue. overflow-wrap:anywhere lets a long key
   prefix or IPv6 literal break mid-token at 320px instead of overflowing
   (UX §8.3). */
code {
  color: inherit;
  font-size: var(--ndx-fs-code);
  overflow-wrap: anywhere;
}

:not(pre) > code {
  padding: 0.08em 0.32em;
  background: var(--ndx-sunken);
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius-sm);
}

/* ==========================================================================
   Layout bands (UX §6: full-bleed sections + vertical rhythm)
   ========================================================================== */

/* Vertical rhythm. Sections own only their TOP space and adjacent
   sections share one hairline, so two stacked sections never produce the
   160px void the old "5rem bottom + 5rem top" arithmetic did (2026-08-23
   polish round, craft review #5). The hairline is the section grammar:
   eyebrow/heading -> content, separated by one 1px rule, every time. The
   first section after the hero gets the rule too; the hero itself has no
   rule above it. Templates must NOT patch spacing with pt-0/mt-* utilities
   -- that was the previous arrangement and it is what made the page read
   as a stack of unrelated objects. */
.ndx-section {
  padding-block: 2.5rem 0;
  margin-block-end: 2.5rem;
  border-block-start: 1px solid var(--ndx-border);
}

/* The landing/pricing header band. Paper, no texture: the 24px grid and
   the scanline overlay that used to live here were removed in the
   2026-08-23 polish round as the most generic "developer tool" device on
   the page (owner's call: "too cliché -- do something less obvious or do
   nothing"). Nothing is the answer; whitespace and the display size carry
   it. Do not reintroduce a background pattern without a new owner ruling. */
.ndx-hero {
  padding-block: 3rem 2.5rem;
}

/* Copy stays readable within the compact split hero. */
.ndx-hero__copy {
  max-inline-size: 44rem;
}

/* The live corpus totals under the hero banner (2026-09-17, core/home.html).
   Paper and hairlines like the rest of that page: a rule above the row, a
   rule between the figures, no plate, no band and no chart -- the trend line
   stays on /top/, which already loads Chart.js for it. Capped to the
   banner's own 960px (.ndx-term--hero) so the numbers sit under the frame
   instead of running the full container width. */
.ndx-totals {
  max-inline-size: 960px;
  margin-block-start: 2rem;
}

.ndx-totals__figures {
  display: grid;
  gap: 0 1.5rem;
  margin-block: 0.75rem 0.6rem;
  border-block-start: 1px solid var(--ndx-border);
}

/* One figure: label over a display-size number -- the readout shape of the
   app's .ndx-stat card (§6.7) without the card, since the marketing page has
   no cards. Stacked with a hairline between at phone width; three columns
   with the hairline between them from md up. */
.ndx-totals__figure {
  padding-block: 0.9rem;
}

.ndx-totals__figure + .ndx-totals__figure {
  border-block-start: 1px solid var(--ndx-border);
}

.ndx-totals__figure dt {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-eyebrow);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ndx-ink-muted);
}

/* `tabular-nums`: three counts of different magnitudes are read as a set,
   and proportional digits make them look mis-aligned in the same row. */
.ndx-totals__figure dd {
  margin: 0;
  font-family: var(--ndx-display);
  font-size: 1.75rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

/* The `~` is a marker, not an abbreviation cue: Bootstrap's dotted underline
   on abbr[title] reads as a typo next to a number. */
.ndx-totals__figure abbr[title] {
  text-decoration: none;
  cursor: help;
}

.ndx-totals__note {
  margin: 0;
  font-size: var(--ndx-fs-small);
  color: var(--ndx-ink-muted);
}

@media (min-width: 768px) {
  .ndx-totals__figures {
    grid-template-columns: repeat(3, 1fr);
  }

  .ndx-totals__figure + .ndx-totals__figure {
    border-block-start: 0;
    border-inline-start: 1px solid var(--ndx-border);
    padding-inline-start: 1.5rem;
  }
}

/* Ink bands group the public insights invitation and closing signup CTA. */
.ndx-band {
  padding-block: 3rem;
  padding-inline: 1.5rem;
  background: var(--ndx-ink);
  color: var(--ndx-ink-onink);
  border-block-start: 3px solid var(--ndx-accent);
  border-radius: var(--ndx-radius-lg);
}

.ndx-band h1,
.ndx-band h2,
.ndx-band h3 {
  color: var(--ndx-paper);
}

/* Keep dark primary-button text separate from light links on ink bands. */
.ndx-band a:not(.btn) {
  color: var(--ndx-paper);
  text-decoration-color: var(--ndx-accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.18em;
}

/* Paper callout: a statement set large with a strong rule above and
   below, for the one paragraph on the site that is a promise ("what ndxer
   does not claim"). Replaces the first ink band. No background, no icon:
   the size and the rules are the emphasis. */
.ndx-callout {
  padding-block: 2.5rem;
  margin-block: 2.5rem;
  border-block: 1px solid var(--ndx-border-strong);
}

.ndx-callout p {
  font-size: var(--ndx-fs-lead);
  line-height: 1.5;
}

/* The callout owns its bottom rule, so the section after it must not draw
   a second one 2.5rem below. */
.ndx-callout + .ndx-section {
  border-block-start: 0;
}

/* Three-up feature row: hairline between columns instead of three floating
   paragraphs (brand review #5). Bootstrap's .row/.col-md-4 still do the
   wrapping; this only draws the rule on the columns from md up. */
@media (min-width: 768px) {
  .ndx-section {
    padding-block: 4rem 0;
    margin-block-end: 4rem;
  }

  .ndx-hero {
    padding-block: 4rem 3rem;
  }

  .ndx-band {
    padding-block: 4rem;
    padding-inline: 2.5rem;
  }

  .ndx-callout {
    padding-block: 3rem;
    margin-block: 4rem;
  }

  .ndx-features > [class*="col-"] + [class*="col-"] {
    border-inline-start: 1px solid var(--ndx-border);
    padding-inline-start: 1.5rem;
  }
}

/* ==========================================================================
   Navbar (UX §5.3)
   ========================================================================== */

/* The 3px accent rule at the top of every page: the single loudest brand
   mark on the site, and it costs no markup. Implemented as an inset shadow
   so it does not disturb the navbar's own border-bottom. */
.navbar {
  --bs-navbar-color: var(--ndx-ink-muted);
  --bs-navbar-hover-color: var(--ndx-ink);
  --bs-navbar-active-color: var(--ndx-ink);
  --bs-navbar-brand-color: var(--ndx-ink);
  --bs-navbar-brand-hover-color: var(--ndx-ink);
  --bs-navbar-padding-y: 0.6rem;

  background: var(--ndx-paper);
  border-bottom: 1px solid var(--ndx-border);
  box-shadow: inset 0 3px 0 var(--ndx-accent);
}

.navbar .nav-link {
  position: relative;
  font-size: var(--ndx-fs-small);
  font-weight: 500;
  padding-inline: 0.7rem;
}

/* Active page: ink text + an accent-deep rule drawn ON the navbar's bottom
   edge (not inside the link's padding box, where it floated 8px above the
   navbar rule -- craft review #12). Two signals (weight and underline) so it
   is never colour-alone; aria-current is already on the element
   (components/nav.html). The offset reaches past the link's own padding and
   the navbar's padding-y to sit on the 1px border. Only from md up, where
   the links are in a row; in the collapsed menu they stack and the rule
   would draw between items. */
.navbar .nav-link[aria-current="page"] {
  color: var(--ndx-ink);
  font-weight: 600;
}

@media (min-width: 768px) {
  .navbar .nav-link[aria-current="page"]::after {
    content: "";
    position: absolute;
    inset-inline: 0.7rem;
    inset-block-end: calc(-1 * var(--bs-navbar-padding-y) - 1px);
    block-size: 2px;
    background: var(--ndx-accent-deep);
  }
}

/* Anonymous nav: "Sign up" is the one button in the bar so the route into
   the product is visible without reading the link labels (UX review #11);
   "Log in" stays a text link. */
.navbar .ndx-nav-cta {
  margin-inline-start: 0.35rem;
}

.dropdown-menu {
  --bs-dropdown-border-color: var(--ndx-border);
  --bs-dropdown-border-radius: var(--ndx-radius);
  --bs-dropdown-link-active-bg: var(--ndx-accent-soft);
  --bs-dropdown-link-active-color: var(--ndx-ink);
  --bs-dropdown-font-size: var(--ndx-fs-small);

  box-shadow: var(--ndx-shadow-pop);
}

/* The account menu shows an email address -- mono, and truncated so a long
   address cannot blow out the navbar at 768px. */
#ndx-account-menu {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

/* The ellipsis lives on an inner span so the toggle's own ::after caret is
   not clipped by overflow: hidden (it rendered as a stub before). */
#ndx-account-menu > .ndx-nav-email {
  max-inline-size: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ==========================================================================
   Buttons (UX §5.4)
   ========================================================================== */

/* Shared geometry. Square-ish, mono-adjacent weight, no gradient. */
.btn {
  --bs-btn-border-radius: var(--ndx-radius);
  --bs-btn-font-size: var(--ndx-fs-small);
  --bs-btn-font-weight: 500;
  --bs-btn-padding-x: 1rem;
  --bs-btn-padding-y: 0.5rem;
  --bs-btn-focus-shadow-rgb: 231, 71, 34;
}

.btn-lg {
  --bs-btn-font-size: var(--ndx-fs-body);
  --bs-btn-padding-x: 1.35rem;
  --bs-btn-padding-y: 0.65rem;
}

.btn-sm {
  --bs-btn-font-size: var(--ndx-fs-meta);
  --bs-btn-padding-x: 0.7rem;
  --bs-btn-padding-y: 0.3rem;
}

/* Primary actions: bright orange with dark text, including hover and press. */
.btn-primary {
  --bs-btn-color: var(--ndx-accent-ink);
  --bs-btn-bg: var(--ndx-accent);
  --bs-btn-border-color: var(--ndx-accent-press);
  --bs-btn-hover-color: var(--ndx-accent-ink);
  --bs-btn-hover-bg: var(--ndx-accent-hover);
  --bs-btn-hover-border-color: var(--ndx-accent-press);
  --bs-btn-active-color: var(--ndx-accent-ink);
  --bs-btn-active-bg: var(--ndx-accent-press);
  --bs-btn-active-border-color: var(--ndx-accent-deep);
  --bs-btn-active-shadow: none;
  --bs-btn-disabled-color: var(--ndx-ink-muted);
  --bs-btn-disabled-bg: var(--ndx-sunken);
  --bs-btn-disabled-border-color: var(--ndx-border);
  --bs-btn-focus-shadow-rgb: 231, 71, 34;
}

/* SECONDARY = ink outline. Both Bootstrap outline classes map here so the
   existing templates (pricing toggle uses .btn-outline-secondary, plan_card
   uses .btn-outline-primary) need no edits. */
.btn-outline-primary,
.btn-outline-secondary {
  --bs-btn-color: var(--ndx-ink);
  --bs-btn-border-color: var(--ndx-border-strong);
  --bs-btn-bg: transparent;
  --bs-btn-hover-color: var(--ndx-ink);
  --bs-btn-hover-bg: var(--ndx-accent-soft);
  --bs-btn-hover-border-color: var(--ndx-ink);
  --bs-btn-active-color: var(--ndx-ink);
  --bs-btn-active-bg: var(--ndx-accent-soft);
  --bs-btn-active-border-color: var(--ndx-ink);
  --bs-btn-active-shadow: none;
  --bs-btn-disabled-color: var(--ndx-ink-muted);
  --bs-btn-disabled-border-color: var(--ndx-border);
  --bs-gradient: none;
}

/* Pressed state of the pricing cycle toggle (.btn-group > .active). */
.btn-group > .btn.active {
  --bs-btn-active-bg: var(--ndx-ink);
  --bs-btn-active-color: var(--ndx-paper);
  --bs-btn-active-border-color: var(--ndx-ink);
}

/* TERTIARY = .btn-link, ink with the prose underline treatment. */
.btn-link {
  --bs-btn-color: var(--ndx-ink);
  --bs-btn-hover-color: var(--ndx-ink);

  text-decoration-color: var(--ndx-accent-deep);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.18em;
}

/* DESTRUCTIVE (revoke a key, cancel a subscription). */
.btn-danger {
  --bs-btn-color: #ffffff;
  --bs-btn-bg: var(--ndx-danger);
  --bs-btn-border-color: var(--ndx-danger);
  --bs-btn-hover-color: #ffffff;
  --bs-btn-hover-bg: #a5121f;
  --bs-btn-hover-border-color: #a5121f;
  --bs-btn-active-bg: #8f0f1a;
  --bs-btn-active-border-color: #8f0f1a;
  --bs-btn-focus-shadow-rgb: 192, 24, 42;
}



/* ==========================================================================
   Cards and plan cards (UX §5.5)
   ========================================================================== */

.card {
  --bs-card-border-color: var(--ndx-border);
  --bs-card-border-radius: var(--ndx-radius-lg);
  --bs-card-inner-border-radius: var(--ndx-radius);
  --bs-card-bg: var(--ndx-surface);
  --bs-card-cap-bg: var(--ndx-sunken);
  --bs-card-spacer-y: 1.15rem;
  --bs-card-spacer-x: 1.15rem;
  --bs-card-title-spacer-y: 0.6rem;

  box-shadow: none; /* cards are bordered, never elevated */
}

.card-title {
  font-family: var(--ndx-display);
  letter-spacing: -0.01em;
}

/* ---- plan card ---- */
.ndx-plan {
  position: relative;
  block-size: 100%;
}

.ndx-plan .card-title {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-eyebrow);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ndx-ink-muted);
}

.ndx-plan__price {
  font-family: var(--ndx-display);
  font-size: 3rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
}

.ndx-plan__cycle {
  font-size: var(--ndx-fs-small);
  color: var(--ndx-ink-muted);
}

/* Every quota number is mono; the words around it are not. Each line is
   ruled so the list reads as a spec sheet, not a paragraph that happens
   to wrap (craft review #7). */
.ndx-plan__limits {
  font-size: var(--ndx-fs-small);
  line-height: 1.5;
}

.ndx-plan__limits li {
  padding-block: 0.4rem;
  border-block-start: 1px solid var(--ndx-border);
}

.ndx-plan__limits li:first-child {
  border-block-start: 0;
}

.ndx-plan__limits b {
  font-family: var(--ndx-mono);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* 2026-08-23 round 2: the card carries three facts as a ruled <dl>
   (label left, mono number right) instead of the seven-line bullet list
   -- the full comparison lives only in .ndx-compare on /pricing/.
   .ndx-plan__limits above is kept for any other caller; plan_card.html
   no longer uses it. */
.ndx-plan__pitch {
  font-size: var(--ndx-fs-small);
  margin-block-end: 1rem;
}

.ndx-plan__facts {
  margin: 0;
  font-size: var(--ndx-fs-small);
}

.ndx-plan__facts > div {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.45rem;
  border-block-start: 1px solid var(--ndx-border);
}

.ndx-plan__facts > div:last-child {
  border-block-end: 1px solid var(--ndx-border);
}

.ndx-plan__facts dt {
  font-weight: 400;
  color: var(--ndx-ink-muted);
}

.ndx-plan__facts dd {
  margin: 0;
  font-family: var(--ndx-mono);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The featured tier uses a deep-orange border and a bright accent ribbon. */
.ndx-plan--featured {
  border: 2px solid var(--ndx-accent-deep);
}

.ndx-plan__ribbon {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: 100%;
  block-size: 4px;
  background: var(--ndx-accent);
  border-radius: var(--ndx-radius-lg) var(--ndx-radius-lg) 0 0;
}

/* "Current plan" state: sunken plate + dashed border, so it cannot be
   confused with the featured tier's solid accent-deep border (craft review
   #8 -- they used to be the same visual). The card also prints "You are on
   this plan." where the other cards have their button, so its bottom does
   not sit empty. */
.ndx-plan--current {
  background: var(--ndx-sunken);
  border: 1px dashed var(--ndx-border-strong);
}

.ndx-plan__current-note {
  font-size: var(--ndx-fs-small);
  color: var(--ndx-ink-muted);
}

/* ==========================================================================
   Tables (UX §5.6)
   ========================================================================== */

.table {
  --bs-table-bg: var(--ndx-surface);
  --bs-table-color: var(--ndx-ink);
  --bs-table-border-color: var(--ndx-border);
  --bs-table-striped-bg: #fafbf8;
  --bs-table-striped-color: var(--ndx-ink);
  --bs-table-hover-bg: var(--ndx-accent-soft);
  --bs-table-hover-color: var(--ndx-ink);
}

.ndx-table {
  font-size: var(--ndx-fs-table);
}

/* Sample tables inside terminal cards (core/home.html): no .table-responsive
   wrapper (its overflow:auto grew a vertical scrollbar around three rows --
   craft review #1) and no Bootstrap .table tokens, so the geometry is set
   here. */
.ndx-term__body .ndx-table {
  inline-size: 100%;
  margin: 0;
  border-collapse: collapse;
}

.ndx-term__body .ndx-table th,
.ndx-term__body .ndx-table td {
  padding: 0.3rem 0.5rem;
  border-block-end: 1px solid var(--ndx-border);
  text-align: start;
}

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

.ndx-term__body .ndx-table th {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-th);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ndx-ink-muted);
  background: var(--ndx-sunken);
}

.ndx-term__body .ndx-table td {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-code);
}

.ndx-table code {
  font-size: 0.8125rem;
}

/* Header row: mono, uppercase, muted, hairline under. */
.ndx-table > thead th {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-th);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ndx-ink-muted);
  background: var(--ndx-sunken);
  border-bottom: 1px solid var(--ndx-border);
  white-space: nowrap;
}

.ndx-table--sticky > thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: 2;
}

.ndx-table > tbody td {
  padding-block: 0.45rem;
  vertical-align: middle;
}

/* Data columns render mono; add .ndx-col-data to the th/td. */
.ndx-col-data {
  font-family: var(--ndx-mono);
  overflow-wrap: anywhere;
}

/* Find domains uses the shared Investigations row table, with these controls
   only on the discovery page. */
.ndx-find-domains__filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.ndx-find-domains__filters [aria-pressed="true"] {
  background: var(--ndx-accent-soft);
  color: var(--ndx-accent-deep);
  border-color: var(--ndx-accent-deep);
  box-shadow: inset 0 -2px 0 var(--ndx-accent-deep);
}

/* The lookup-type strip (components/lookup_modes.html): five links across the
   top of every signed-in lookup page. Flex-wrap rather than a scroller so all
   five stay reachable at 375px without a horizontal gesture, and the current
   one is marked by weight AND an underline, never by colour alone (UX §8).
   Gap and padding keep every link's hit area past the 24px floor the e2e tap
   target check enforces. */
/* The lookup tab bar (components/lookup_modes.html), 2026-09-17.

   It replaces a strip of five underlined text links that sat directly above a
   two-button segmented toggle inside the form, which repeated Current DNS and
   Inverse IP -- two navigations for overlapping things on one screen. The
   owner's fix: one navigation, "nice tabs". So all five read as tabs (a plate,
   a border, one hit area each) and the current one is FILLED, which is what a
   strip of links could not say.

   Links styled as tabs, never `role="tablist"`: five URLs, not five panels.
   The markup rationale is in the template's own comment.

   Wrapping, not scrolling: five labels do not fit one 375px row, and a
   horizontally-scrolled tab rail hides tabs behind an edge with no affordance.
   `flex-wrap` puts them on two rows at 375 and one at 1440. */
/* The shared tab bar: one set of rules, two consumers.

   `components/lookup_modes.html` (the five signed-in lookup pages) and
   `top/_family_tabs.html` (the four `/top/` family pages, 2026-09-18) render
   the same control -- a row of links, one of them `aria-current="page"` --
   so they render the same class rather than two stylesheet blocks that drift.
   Renamed from `.ndx-lookup-modes` when the second consumer arrived; no alias
   class was left behind, because a class no template writes is dead CSS a
   later reader has to prove is unused.

   `flex-wrap: wrap`, never a horizontal scroller: a tab bar that scrolls
   sideways hides tabs at 375px and trips the e2e overflow check on every
   page that carries one. */
.ndx-tab-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}

/* `.ndx-tab-bar` repeated on the selector, not the bare class: these
   pages render inside `.ndx-prose`, whose `a:not(.btn, ...)` rule is
   (0,2,1) -- one class MORE specific than a bare `.ndx-tab-bar__link`
   would be -- and it paints every link with the prose underline. Scoping
   alone was NOT enough when this was fixed for the lookup tab bar (2026-09-17):
   `.ndx-tab-bar .ndx-tab-bar__link` is (0,2,0) and still lost, which is why
   those links rendered underlined and the current one was left marked by font
   weight alone. The durable half of the fix is that
   `.ndx-tab-bar__link` is in that rule's own `:not()` list, beside
   `.nav-link` and `.page-link` -- the same category of thing, a navigation
   control rather than a link in a sentence. Both halves are kept: the
   `:not()` entry is what actually wins, the repeated class is what keeps
   these declarations from leaking onto any other anchor.

   `min-block-size: 2.75rem` is 44px, WCAG 2.5.5's target size, not the 24px
   floor UX §8.3 sets for the site: this is the primary navigation of a
   section on a page people use from a phone, and tabs of different heights
   would also make a wrapped second row look accidental.
   `flex` + `align-items: center` rather than vertical padding so the
   tabs are the same height whatever their label. */
.ndx-tab-bar .ndx-tab-bar__link {
  display: inline-flex;
  align-items: center;
  min-block-size: 2.75rem;
  padding-inline: 0.85rem;
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius-sm);
  background: var(--ndx-sunken);
  font-size: 0.9375rem;
  line-height: 1.2;
  white-space: nowrap;
  text-decoration: none;
  color: var(--ndx-ink-muted);
}

.ndx-tab-bar .ndx-tab-bar__link:hover {
  background: var(--ndx-surface);
  border-color: var(--ndx-border-strong);
  color: var(--ndx-ink);
}

/* The current tab: an ink plate with the 3px orange edge the navbar wears at
   the top of every page. Three signals, never colour alone (UX §8) -- the
   fill, the weight, and `aria-current="page"` in the markup, which is what a
   screen reader and a forced-colours mode actually read. Orange is FILL here,
   on an ink surface, which is the one place roadmap B3 allows it to be loud;
   the text is --ndx-ink-onink, not orange. */
.ndx-tab-bar .ndx-tab-bar__link[aria-current="page"] {
  background: var(--ndx-ink);
  border-color: var(--ndx-ink);
  color: var(--ndx-ink-onink);
  font-weight: 600;
  box-shadow: inset 0 3px 0 var(--ndx-accent);
}

.ndx-tab-bar .ndx-tab-bar__link[aria-current="page"]:hover {
  background: var(--ndx-ink);
  border-color: var(--ndx-ink);
  color: var(--ndx-ink-onink);
}

/* D43: a single native Lookup mode menu on phones. */
.ndx-lookup-menu > summary {
  display: list-item;
  inline-size: fit-content;
  list-style-position: inside;
}
.ndx-lookup-menu > nav {
  flex-direction: column;
}
.ndx-lookup-menu .ndx-tab-bar__link {
  inline-size: 100%;
}

/* The `/top/` TLD filter (2026-09-18): label, select and submit button on one
   line, wrapping to three rows at 375px rather than pushing the page sideways.
   `align-items: end` lines the control and the button up on their baselines
   while the label sits above the select, which is where Bootstrap's
   `.form-label` puts it.

   `max-inline-size` on the select because a twenty-item menu of three-letter
   TLDs does not need half the viewport, and an unconstrained `.form-select`
   in a flex row takes whatever is left. */
.ndx-tld-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 0.5rem 0.75rem;
  margin-block-end: 1.5rem;
}

.ndx-tld-filter .form-label {
  margin-block-end: 0.25rem;
}

.ndx-tld-filter .form-select {
  max-inline-size: 14rem;
}

/* Which TLD a filtered panel's figures are about, inside the meta strip. The
   strip already separates its items with a "·"; the weight is the only thing
   this adds, so the scope is not read as just another timestamp. Never
   colour-alone -- it is a sentence naming the TLD. */
.ndx-scoped {
  font-weight: 500;
}

/* The value column of a value-lookup result (components/name_match_table.html).
   A TXT record can legitimately be 765 bytes on one row (wire-format.md: three
   255-byte strings concatenated), and `overflow-wrap: anywhere` alone does not
   stop a table cell asking for its max-content width -- so one long row would
   push the whole table sideways inside .table-responsive and turn every other
   row into a horizontal scroll. Capping the column makes the long value wrap
   and leaves the name, type and date where a reader expects them. `ch` rather
   than `rem`: the cell is mono, so the cap is in characters of the font that
   actually renders it. */
.ndx-col-value {
  max-width: 56ch;
}

/* The name column of a current-DNS result (components/dns_result_table.html),
   2026-09-18 (owner's site walk, item 2). Every row of that table repeats one
   hostname, and `.ndx-col-data`'s `overflow-wrap: anywhere` broke it mid-label
   at 375 -- "www.a-long-name.example" came back as three lines and, because
   eight rows all carry a name, tripled the height of the whole table. A
   hostname has no break point a reader can use: the dot is not a hyphen, and a
   name split after "www." reads as a different name. Same opt-out, same
   reasoning and same shape as `.ndx-history-when` (the dashboard card's
   timestamp column, 2026-09-17) -- and the sanctioned way to leave
   `.ndx-col-data`: add a second class to the one cell, never weaken the base
   rule, which the value column beside it still needs. The extra width is
   absorbed by `.ndx-col-value` on the value cell (capped, so the long value
   wraps instead of the table widening) and, past that, scrolls inside
   `.table-responsive` -- so the page itself never scrolls sideways, which is
   what `tests/e2e/test_pages.py`'s overflow guard holds every page to.
   Verified in a browser at 375 and 1440; pinned by
   `tests/e2e/test_lookup.py::test_every_result_name_is_one_line_at_375`,
   because a stylesheet cannot tell you a cell rendered on two lines --
   `.ndx-history-when` learned that the hard way. Measured before and after:
   without this rule each name renders across THIRTEEN line boxes at 375
   (`.ndx-col-data` breaks a hostname a character or two at a time), with it,
   one. That test counts them with a `Range`, not with the cell's own
   `getClientRects()` -- see its docstring for why the obvious form is
   vacuous on a `<td>`. */
.ndx-col-name {
  overflow-wrap: normal;
  white-space: nowrap;
}

/* Numeric columns align right and use tabular figures so quota digits
   line up column-wise. */
.ndx-col-num {
  text-align: end;
  font-variant-numeric: tabular-nums;
  font-family: var(--ndx-mono);
}

/* The pricing compare table (UX §6.2). A .ndx-table with a sticky row-header
   column, so the row label stays readable while the four tier columns scroll
   inside .table-responsive at narrow widths. Every value cell is mono and
   right-aligned: the cells hold numbers, or the literal words YES / — (never
   an icon and never colour alone -- UX §8.1 greyscale rule). */
.ndx-compare {
  /* Five columns over the full 1140px container put values 300px from
     their labels (craft review #9); the table is capped and the row-label
     column sized so the numbers sit near the words. */
  max-inline-size: 760px;
}

.ndx-compare th[scope="row"] {
  inline-size: 40%;
  position: sticky;
  inset-inline-start: 0;
  z-index: 1;
  background: var(--ndx-surface);
  font-weight: 500;
  white-space: nowrap;
}

.ndx-compare > tbody td {
  text-align: end;
  font-family: var(--ndx-mono);
  font-variant-numeric: tabular-nums;
}

/* Row-group label (a full-width <th colspan> starting "Quotas", "Limits"…). */
.ndx-compare__group th {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-th);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ndx-ink-muted);
  background: var(--ndx-sunken);
}

/* ==========================================================================
   code_block.html (UX §5.7)
   ========================================================================== */

.ndx-code-block {
  margin-block-end: 1rem;
}

/* The "bash" label above the block becomes a mono chip on the block's own
   top edge rather than floating text. */
.ndx-code-block__lang {
  display: inline-block;
  padding: 0.15rem 0.45rem;
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ndx-ink-muted);
  background: var(--ndx-sunken);
  border: 1px solid var(--ndx-border);
  border-block-end: 0;
  border-radius: var(--ndx-radius) var(--ndx-radius) 0 0;
}

.ndx-code-block__body {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.85rem 1rem;
  background: var(--ndx-sunken);
  border: 1px solid var(--ndx-border);
  border-radius: 0 var(--ndx-radius) var(--ndx-radius) var(--ndx-radius);
}

.ndx-code-block pre {
  margin: 0;
  flex: 1 1 auto;
  font-size: var(--ndx-fs-code);
  line-height: 1.55;
  overflow-x: auto; /* horizontal scroll on the block ... */
}

.ndx-code-block code {
  background: none;
  border: 0;
  padding: 0;
}

/* ... except the shell one-liner, which wraps with a hanging indent so a
   long curl stays readable at 320px instead of scrolling sideways. */
.ndx-code-block--wrap pre {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  text-indent: -2ch;
  padding-inline-start: 2ch;
}

/* Shell prompt, pseudo-element so it is never copied (see §4.3). */
.ndx-code-block--shell pre::before {
  content: "$\00a0";
  color: var(--ndx-accent-deep);
  user-select: none;
}

/* Tap-target floor for the copy button wherever it appears (UX §8.3:
   >= 24x24px, including the quiet one inside a code block). */
.js-copy {
  min-inline-size: 24px;
  min-block-size: 24px;
  display: inline-flex;
  align-items: center;
  flex: none; /* never squeezed narrower than its label inside a flex row (e2e mobile pass) */
  white-space: nowrap;
  text-decoration: none; /* a control, not a prose link (craft review #19) */
}

/* The copy button is quiet until you approach the block; always visible on
   touch, where there is no hover. */
.ndx-code-block .js-copy {

  color: var(--ndx-ink-muted);
  transition: color 120ms linear;
}

.ndx-code-block:hover .js-copy,
.ndx-code-block .js-copy:focus-visible {
  color: var(--ndx-ink);
}

@media (hover: none) {
  .ndx-code-block .js-copy {
    color: var(--ndx-ink);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ndx-code-block .js-copy {
    transition: none;
  }
}

/* ==========================================================================
   Badges (UX §5.8)
   ========================================================================== */

/* House badge: mono, uppercase, square, soft fill + deep text. Never a
   pill -- pills read consumer-app; squares read instrument panel. */
.badge {
  --bs-badge-border-radius: var(--ndx-radius-sm);
  --bs-badge-font-size: 0.6875rem;
  --bs-badge-font-weight: 500;
  --bs-badge-padding-x: 0.45em;
  --bs-badge-padding-y: 0.3em;

  font-family: var(--ndx-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: 1px solid transparent;
}

/* MANDATORY: Bootstrap hard-codes color:#fff!important on .text-bg-*, which
   is measured contrast on the orange primary. plan_card.html already uses
   .text-bg-primary for "Current plan". */
.badge.text-bg-primary {
  color: var(--ndx-ink) !important;
  background-color: var(--ndx-accent-soft) !important;
  border-color: var(--ndx-accent-press);
}

.badge.text-bg-secondary {
  color: var(--ndx-ink-muted) !important;
  background-color: var(--ndx-sunken) !important;
  border-color: var(--ndx-border);
}

.badge.text-bg-success {
  color: var(--ndx-success) !important;
  background-color: var(--ndx-success-soft) !important;
  border-color: #bfe3c9;
}

.badge.text-bg-warning {
  color: var(--ndx-warning) !important;
  background-color: var(--ndx-warning-soft) !important;
  border-color: #ecd79a;
}

.badge.text-bg-danger {
  color: var(--ndx-danger) !important;
  background-color: var(--ndx-danger-soft) !important;
  border-color: #f2c4c0;
}

.badge.text-bg-info {
  color: var(--ndx-info) !important;
  background-color: var(--ndx-info-soft) !important;
  border-color: #bcd8fb;
}

/* Confidence badge on the related lookup's result table
   (components/related_table.html, 2026-09-26, roadmap D37).

   Its own class rather than `.text-bg-success`/`.text-bg-warning`: those
   three are the product's STATUS colours (a key revoked, a quota exceeded, a
   payment failed) and a confidence band is not a status -- a "low" band is a
   perfectly good answer, not a warning, and painting it amber would tell a
   reader something went wrong. The three steps here are one hue at three
   weights, which reads as a scale rather than as a traffic light.

   Note `.ndx-band` is already taken in this stylesheet (the full-bleed page
   bands, line ~412) and is deliberately NOT reused here. */
.ndx-confidence {
  color: var(--ndx-ink-muted);
  background-color: var(--ndx-sunken);
  border: 1px solid var(--ndx-border);
  font-weight: 500;
}

.ndx-confidence--high {
  color: var(--ndx-ink);
  background-color: var(--ndx-accent-soft);
  border-color: var(--ndx-accent-press);
}

.ndx-confidence--medium {
  color: var(--ndx-ink);
  background-color: var(--ndx-sunken);
  border-color: var(--ndx-border-strong);
}

/* ==========================================================================
   Alerts (UX §5.9)
   ========================================================================== */

/* Correctness banners are the one place this product raises its voice, so
   they are a left rule + soft plate + INK body text. Coloured body text on
   a coloured plate is harder to read and buys nothing: the rule, the icon
   and the heading already carry the level. */
.alert {
  --bs-alert-border-radius: var(--ndx-radius);
  --bs-alert-padding-x: 1rem;
  --bs-alert-padding-y: 0.75rem;

  border: 1px solid var(--ndx-border);
  border-inline-start: 3px solid var(--ndx-border-strong);
  color: var(--ndx-ink);
  font-size: var(--ndx-fs-small);
}

.alert .alert-heading {
  font-family: var(--ndx-display);
  font-size: var(--ndx-fs-h4);
  color: inherit;
}

.alert-success {
  background: var(--ndx-success-soft);
  border-inline-start-color: var(--ndx-success);
}

.alert-warning {
  background: var(--ndx-warning-soft);
  border-inline-start-color: var(--ndx-warning);
}

.alert-danger {
  background: var(--ndx-danger-soft);
  border-inline-start-color: var(--ndx-danger);
}

.alert-info {
  background: var(--ndx-info-soft);
  border-inline-start-color: var(--ndx-info);
}

.alert-primary {
  background: var(--ndx-accent-soft);
  border-inline-start-color: var(--ndx-accent-deep);
}

/* components/alert.html emits .alert-debug / .alert-error for Django's
   message tags; keep them mapped (replaces the two stray rules at the
   bottom of the pre-2026-08-22 project.css, which used raw Bootstrap-3
   hexes). */
.alert-error {
  background: var(--ndx-danger-soft);
  border-inline-start-color: var(--ndx-danger);
}

.alert-debug {
  background: var(--ndx-sunken);
  border-inline-start-color: var(--ndx-border-strong);
}

/* ==========================================================================
   Forms (UX §5.10)
   ========================================================================== */

.form-label {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ndx-ink-muted);
  margin-block-end: 0.3rem;
}

/* Exception: a sentence-shaped label (a terms-acceptance checkbox, a
   consent line) stays in the body face at body size. Uppercase mono is for
   NAMES of fields, never for prose. */
.form-check .form-label,
.form-check-label {
  font-family: var(--ndx-sans);
  font-size: var(--ndx-fs-body);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ndx-ink);
}

.form-control,
.form-select {
  font-size: var(--ndx-fs-body);
  color: var(--ndx-ink);
  background-color: var(--ndx-surface);
  border: 1px solid var(--ndx-border-strong);
  border-radius: var(--ndx-radius);
  padding: 0.5rem 0.7rem;
}

/* Anything you type a hostname, an IP, or a key into is mono. */
.ndx-input-data,
input[type="email"].ndx-input-data {
  font-family: var(--ndx-mono);
}

.form-control::placeholder {
  color: var(--ndx-ink-muted);
  opacity: 1;
}

/* Bootstrap hard-codes #86b7fe + a blue glow here. */
.form-control:focus,
.form-select:focus {
  border-color: var(--ndx-ink);
  outline: 2px solid var(--ndx-ink);
  outline-offset: 1px;
  box-shadow: 0 0 0 5px var(--ndx-accent-soft);
}

.form-control.is-invalid,
.was-validated .form-control:invalid {
  border-color: var(--ndx-danger);
}

.invalid-feedback {
  color: var(--ndx-danger);
  font-size: var(--ndx-fs-small);
}

.form-text {
  color: var(--ndx-ink-muted);
  font-size: var(--ndx-fs-small);
}

/* Checked inputs use a dark plate and a light glyph. */
.form-check-input {
  border-color: var(--ndx-border-strong);
  border-radius: var(--ndx-radius-sm);
}

.form-check-input:checked {
  background-color: var(--ndx-ink);
  border-color: var(--ndx-ink);
}

.form-check-input:checked[type="checkbox"] {
  --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23F0EFEB' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");
}

.form-check-input:checked[type="radio"] {
  --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23F0EFEB'/%3e%3c/svg%3e");
}

/* Large lookup input (M5). Mono, 18px, ink border -- the one oversized
   control on the site. */
.ndx-lookup-input {
  font-family: var(--ndx-mono);
  font-size: 1.125rem;
  padding: 0.7rem 0.9rem;
  border-width: 2px;
  border-color: var(--ndx-ink);
}

/* ==========================================================================
   Accordion, progress, footer (UX §5.11)
   ========================================================================== */

.accordion {
  --bs-accordion-border-color: var(--ndx-border);
  --bs-accordion-border-radius: var(--ndx-radius);
  --bs-accordion-btn-color: var(--ndx-ink);
  --bs-accordion-btn-bg: var(--ndx-surface);
  --bs-accordion-active-color: var(--ndx-ink);
  --bs-accordion-active-bg: var(--ndx-accent-soft);
  --bs-accordion-btn-focus-box-shadow: 0 0 0 5px var(--ndx-accent-soft);
  --bs-accordion-body-padding-y: 0.9rem;

  /* Both chevrons are recoloured: Bootstrap bakes the hex into the data
     URI, and the "active" one ships as Bootstrap blue. */
  --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2312161B' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
  --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23A83215' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}

.accordion-button {
  font-family: var(--ndx-display);
  font-size: var(--ndx-fs-h4);
  font-weight: 600;
}

/* Usage data uses the deep accent so the fill stays distinct from its track. */
.progress {
  --bs-progress-bg: var(--ndx-sunken);
  --bs-progress-border-radius: var(--ndx-radius-sm);
  --bs-progress-height: 0.625rem;
  --bs-progress-bar-bg: var(--ndx-accent-deep);

  border: 1px solid var(--ndx-border-strong); /* the empty track must still read as a meter, not a divider */
}

.progress-bar.bg-warning {
  background-color: var(--ndx-warning) !important;
}

.progress-bar.bg-danger {
  background-color: var(--ndx-danger) !important;
}

/* Footer: the second ink band on the site. */
.ndx-footer {
  margin-block-start: 4rem;
  padding-block: 0.75rem;
  background: var(--ndx-ink);
  color: var(--ndx-ink-onink);
  /* No orange rule here: the navbar and the closing .ndx-band already carry
     one each, and a third in the same viewport made the accent meaningless
     (brand review #7). The footer's orange is the static two-direction mark. */
  border-block-start: 1px solid var(--ndx-border-strong);
  font-size: var(--ndx-fs-small);
}

.ndx-footer a {
  color: var(--ndx-paper);
  text-decoration-color: var(--ndx-accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.18em;
}

/* Bright orange remains readable on the dark footer. */
.ndx-footer a:hover {
  color: var(--ndx-accent);
}

.ndx-footer .ndx-eyebrow,
.ndx-band .ndx-eyebrow {
  color: #9aa3ac;
}

/* Compact footer: pronunciation beside the wordmark, navigation and brand
   credit share the second row; touch targets retain their full height. */
.ndx-footer__layout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.35rem 2rem;
}

.ndx-footer__brand {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-basis: 100%;
}

.ndx-footer__brand p {
  margin: 0;
  max-inline-size: none;
  font-size: var(--ndx-fs-meta);
}

.ndx-footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.1rem;
}

.ndx-footer__links a,
.ndx-footer__credit {
  display: inline-flex;
  align-items: center;
  min-block-size: 44px;
}

.ndx-footer__credit {
  gap: 0.55rem;
  white-space: nowrap;
  font-size: var(--ndx-fs-meta);
}

.ndx-footer__credit img {
  flex: none;
  border-radius: 2px;
}

/* ==========================================================================
   Texture devices (UX §4)
   ========================================================================== */

/* §4.1 / §4.2 -- the 24px grid (.ndx-grid) and the scanline overlay
   (.ndx-scan) were REMOVED in the 2026-08-23 polish round. Owner's ruling:
   "the squared background grid is too cliché, don't do that, do something
   less obvious or do nothing." All three design reviews agreed the grid +
   scanlines + window dots were the generic developer-tool costume of the
   period. The choice was "nothing": paper, hairlines, display type. Do not
   add a background texture without a new ruling. The owner explicitly requested
   the contained homepage editorial artwork on 2026-10-04; see the footer-contact-art
   ledger. That exception does not authorize repeating site textures. The design document's
   §4.1/§4.2 are superseded by docs/superpowers/ledgers/2026-08-23-ux-polish-
   roast-ledger.md. */

/* §4.3 Terminal chrome on sample cards: the chrome bar is ink, the body is
   paper. Marketing surfaces only (landing samples, "your first request" on
   the docs quickstart, the error pages) -- never around a form, never around
   real data. Record-shaped examples must explicitly identify themselves as
   illustrative; the compact hero does this in its eyebrow.

   2026-08-23 polish round: the chrome was re-cut. The ink title bar with a
   lime square and two grey "window dots" was the universal "I am a dev
   tool" sticker (brand review #2); it is now a hairline header on the
   sunken plate -- mono label left, badge right -- and the card border is
   the strong hairline rather than full ink. Orange is no longer used on this
   component at all (the bar is paper, where orange may not be text), so the
   badge is an ink-muted mono label on a surface chip. */
.ndx-term {
  border: 1px solid var(--ndx-border-strong);
  border-radius: var(--ndx-radius-lg);
  overflow: hidden;
  background: var(--ndx-surface);
}

.ndx-term__bar {
  display: flex;
  flex-wrap: wrap; /* at 375px a long label + the badge must stack, not overflow the card (mobile pass 2026-08-23) */
  align-items: center;
  gap: 0.25rem 0.5rem;
  padding: 0.5rem 0.85rem;
  background: var(--ndx-sunken);
  color: var(--ndx-ink);
  border-block-end: 1px solid var(--ndx-border);
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  letter-spacing: 0.02em;
}

/* The hero frame (core/home.html) holds the labelled illustrative DNS
   result (.ndx-preview) until the owner records the new product video. It
   is capped so it does not push every proof below the fold at 1440x900
   (craft review #2). */
.ndx-term--hero {
  max-inline-size: 960px;
}


.ndx-term__badge {
  margin-inline-start: auto;
  padding: 0.05em 0.45em;
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius-sm);
  background: var(--ndx-surface);
  color: var(--ndx-ink-muted);
  font-size: var(--ndx-fs-eyebrow);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

.ndx-term__body {
  padding: 0.9rem 1rem;
  overflow-x: auto; /* a four-column sample table scrolls inside the card at 375px; the page never scrolls sideways */
}

/* Prompt. The "$ " is a pseudo-element so it is never selected when the
   reader drags across the command to copy it -- copying a leading "$"
   into a shell is the classic docs paper cut. */
.ndx-term__cmd {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-code);
  margin: 0 0 0.6rem;
  overflow-wrap: anywhere; /* a long curl must wrap at 320px, not overflow */
}

.ndx-term__cmd::before {
  content: "$\00a0";
  color: var(--ndx-accent-deep);
  user-select: none;
}

/* Auth pages (§6.6): one 420px column holding the wordmark, the card, and
   the legal/contact line. allauth/layouts/entrance.html is the only user. */
.ndx-entrance {
  max-inline-size: 420px;
}

/* The auth pages sit on the sunken plate (the grid that used to be their
   background is gone) so the white card still reads as an object. */
.ndx-entrance-page {
  background: var(--ndx-sunken);
}

/* Every field on the auth forms is required, so crispy's "*" suffix is
   noise; it is hidden visually here (the `required` attribute still
   announces). */
.ndx-entrance .asteriskField {
  display: none;
}

.ndx-entrance__legal {
  font-size: var(--ndx-fs-small);
  color: var(--ndx-ink-muted);
}

/* Error pages (§6.8): 403 / 403_csrf / 404 / users/suspended are the one
   place the terminal card is the whole page. 560px so the card reads as an
   object on the grid rather than a full-width slab, and the `<code> <verb>
   <path>` / `<status> <error_code>` pair at the top deliberately mirrors the
   API error envelope's vocabulary (roadmap D23). 500.html cannot use these
   rules -- it must render with no static files at all -- so it repeats the
   same four token values inline; keep the two in sync. */
.ndx-error {
  max-inline-size: 560px;
}

.ndx-error__code {
  margin-block: 0.35rem 0.75rem;
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-code);
  color: var(--ndx-ink-muted);
}

/* §4.5 The monospace metadata strip: the house pattern for "facts about this
   result/object". A readout, so it holds no links and no buttons, and never
   substitutes for a real table. One strip per card. */
.ndx-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
  padding: 0.45rem 0.75rem;
  background: var(--ndx-sunken);
  border-top: 1px solid var(--ndx-border);
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink-muted);
}

.ndx-meta > * + *::before {
  content: "·";
  margin-inline-end: 0.6rem;
  color: var(--ndx-border-strong);
}

/* A state inside the strip is never colour-alone: the word changes too. */
.ndx-meta__flag--on {
  color: var(--ndx-warning);
  font-weight: 500;
}

/* Text selection uses a pale orange surface with dark ink. */
::selection {
  background: var(--ndx-select-bg);
  color: var(--ndx-ink);
}

/* Text insertion cursors use deep orange for visibility on light surfaces. */
input,
textarea {
  caret-color: var(--ndx-accent-deep);
}


.ndx-prose a:not(.btn, .nav-link, .dropdown-item, .accordion-button, .page-link, .ndx-tab-bar__link, .ndx-lookup-action, .ndx-ib) {
  color: var(--ndx-ink);
  text-decoration: underline;
  text-decoration-color: var(--ndx-accent-deep);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.18em;
  text-decoration-skip-ink: auto;
}

.ndx-prose a:not(.btn, .nav-link, .dropdown-item, .accordion-button, .page-link, .ndx-tab-bar__link, .ndx-lookup-action, .ndx-ib):hover {
  background: var(--ndx-accent-soft);
  text-decoration-color: var(--ndx-accent-press);
}


:focus-visible {
  outline: 2px solid var(--ndx-ink);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px var(--ndx-accent-soft);
}

/* Bootstrap's own .btn:focus-visible (0,2,0) would beat the bare pseudo
   above and paint its rgb glow instead; restate the ring at equal
   specificity. */
.btn:focus-visible {
  outline: 2px solid var(--ndx-ink);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px var(--ndx-accent-soft);
}

/* On an ink surface the ring inverts so it stays visible. */
.ndx-band :focus-visible,
.ndx-footer :focus-visible {
  outline-color: var(--ndx-accent);
  box-shadow: 0 0 0 5px rgb(231 71 34 / 25%);
}

/* ==========================================================================
   Wordmark (UX §7.1)
   ========================================================================== */

/* Static bidirectional wordmark; the decorative mark is aria-hidden in HTML. */
.ndx-wordmark {
  font-family: var(--ndx-display);
  font-weight: 600;
  letter-spacing: -0.04em;
  color: var(--ndx-ink);
  text-decoration: none;
  white-space: nowrap;
}

/* The only optical correction: mono's uniform advance makes the leading
   "n" float away from the rest, so pull it back a hair. */
.ndx-wordmark__n {
  margin-inline-end: -0.02em;
}

/* Compact wordmark sizes; the two-direction mark scales with the text. */
.ndx-wordmark--nav {
  font-size: 1.25rem;
}

.ndx-wordmark--hero {
  font-size: 1.5rem; /* auth pages: the card heading is the large element, not the mark */
}

.ndx-wordmark--footer {
  font-size: 1.25rem;
  color: var(--ndx-paper);
}



/* ==========================================================================
   Disclosure widgets (2026-08-23 round 2): native <details>, no JS.
   .ndx-rule   -- one clause of "Rules of the tokens" (components/token_rules.html)
   .ndx-sample -- a folded "Sample response" JSON block (core/docs_quickstart.html)
   The marker is the default triangle (never an icon font); the summary
   is the whole heading line so the tap target is the full width.
   ========================================================================== */
.ndx-rule {
  border-block-start: 1px solid var(--ndx-border);
  padding-block: 0.5rem;
}

.ndx-rule:last-child {
  border-block-end: 1px solid var(--ndx-border);
}

.ndx-rule > summary {
  cursor: pointer;
  padding-block: 0.25rem;
}

/* The <h3> must stay inline with the marker, and keep the outline intact
   for screen readers (summary > h3 is valid HTML). */
.ndx-rule > summary > h3 {
  display: inline;
  margin: 0;
}

.ndx-rule > p {
  margin-block: 0.5rem 0.25rem;
  padding-inline-start: 1.1rem; /* align with the heading text, past the marker */
}

.ndx-sample {
  margin-block: 0.5rem 1rem;
}

.ndx-sample > summary {
  cursor: pointer;
  font-size: var(--ndx-fs-small);
  color: var(--ndx-ink-muted);
  margin-block-end: 0.35rem;
}

.ndx-sample[open] > summary {
  color: var(--ndx-ink);
}

/* ==========================================================================
   Docs page frame (core/docs_quickstart.html, 2026-08-23 round 2): a
   reading column and a sticky right-rail anchor list from lg up. The
   rail's links are quiet (no 2px accent underline) so the page has one
   strong link style -- the prose links -- and one wayfinding style.
   ========================================================================== */
.ndx-docs__rail li {
  padding-block: 0.2rem;
  font-size: var(--ndx-fs-small);
}

.ndx-prose .ndx-docs__rail a:not(.btn, .nav-link, .dropdown-item, .accordion-button, .page-link) {
  text-decoration: none;
  color: var(--ndx-ink-muted);
}

.ndx-prose .ndx-docs__rail a:not(.btn, .nav-link, .dropdown-item, .accordion-button, .page-link):hover {
  background: none;
  color: var(--ndx-ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
}

.ndx-docs__next {
  margin-block-start: 2.5rem;
  padding-block-start: 1.5rem;
  border-block-start: 1px solid var(--ndx-border);
  font-weight: 500;
}

/* Anchor targets stop under the sticky-free navbar with a little air. */
.ndx-docs__body h2[id] {
  scroll-margin-block-start: 1.5rem;
}

@media (min-width: 992px) {
  .ndx-docs__rail {
    position: sticky;
    inset-block-start: 1.5rem;
  }
}

/* ==========================================================================
   Page frame: the footer sits at the bottom of short pages (craft review
   #14 -- on /app/usage/ with one card it floated at 40% of the viewport).
   entrance.html renders its own <main> inside the same body and is fine.
   ========================================================================== */
body {
  min-block-size: 100dvh;
  display: flex;
  flex-direction: column;
}

body > main {
  flex: 1 0 auto;
}

/* ==========================================================================
   App patterns (§6.7): stat card and empty state.
   The marketing site performs; the app reports -- these are the readout
   shapes for /app/*, added in the 2026-08-23 polish round so the logged-in
   pages stop looking like a default Bootstrap card grid.
   ========================================================================== */

/* Stat card: eyebrow (the real <h2>) over a display-size value; actions
   pinned to the bottom so three cards in a row align. */
.ndx-stat .card-body {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.ndx-stat__value {
  font-family: var(--ndx-display);
  font-size: 1.75rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.ndx-stat__actions {
  margin-block-start: auto;
  padding-block-start: 0.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.9rem;
  align-items: center;
}

/* The row's quiet links ("Billing" on the Plan card, "View usage" on the
   Usage card) are plain `.small` text, so their box was just the 22px line
   box of 0.875em type -- under the 24px floor UX §8.3 / WCAG 2.5.8 set for
   every control, and that way since M3-3. Measured at 1440 and 375 in a
   browser on 2026-09-17 (40x22 and 77x22); found while writing the Recent
   lookups card's tap-target sweep, which is why that sweep used to be
   scoped to `#ndx-recent-lookups` and now covers the whole dashboard
   (`tests/e2e/test_dashboard.py::test_the_populated_card_renders_cleanly_at_both_widths`).
   Same shape of fix as `.ndx-history-link` below: an inline-flex box with a
   minimum block size, so the hit area grows without a bigger font, a button
   or a padding change that would move the text off the card's baseline
   rhythm. It cannot disturb what `.ndx-stat__actions` exists for -- the row
   is still pinned to the card's bottom edge by `margin-block-start: auto`,
   so the three cards' action rows stay on one line however long their copy
   is; a row that also holds a `.btn-sm` (the Plan card's "Upgrade", 31px)
   does not change height at all, and the Usage card's row grows 22px -> 24px
   upward into the card's own slack rather than pushing its bottom edge down.
   Verified in a browser at both widths, not only by the e2e sweep. */
.ndx-stat__actions a:not(.btn) {
  display: inline-flex;
  align-items: center;
  min-block-size: 24px;
}

/* Empty state: a dashed sunken plate, centred, one bold line + one muted
   line. Replaces ad-hoc `.border.rounded-2.p-4.text-center` utility soup. */
.ndx-empty {
  padding: 2rem 1.5rem;
  text-align: center;
  background: var(--ndx-sunken);
  border: 1px dashed var(--ndx-border-strong);
  border-radius: var(--ndx-radius-lg);
}

.ndx-empty > :last-child {
  margin-block-end: 0;
}

/* The measure cap from .ndx-prose would left-weight a centred paragraph;
   centre the capped box instead. */
.ndx-empty p {
  margin-inline: auto;
}

/* Dashboard "Recent lookups" card (components/recent_lookups.html,
   2026-09-17). The only genuinely new rule the card needed: an inline <a>
   inside a table row is about 19px tall, under the 24px floor UX §8.3 sets
   for every control -- which is exactly the finding the lookup tab-bar
   ledger (2026-09-17) left open for the result tables' pivot links. An
   inline-flex box with a minimum block size gives the link a real hit area
   without changing the row's rhythm; `align-items: center` keeps the text
   on the row's baseline grid rather than pinned to the top of a taller box.
   No new colour and no new surface: the card is an ordinary `.card` on the
   page's own ground, and the roadmap's B3 ink band is not extended here. */
.ndx-history-link {
  display: inline-flex;
  align-items: center;
  min-block-size: 24px;
}

/* The two small links on a Current DNS result: the per-token pivot links
   inside a record value (components/dns_result_table.html) and the outbound
   registration-data link in the heading above the table
   (components/rdap_link.html). Both landed 2026-09-18, and both are the same
   recipe as .ndx-history-link just above, for the same failure: `.small`
   type at the table's compact row height (.ndx-table > tbody td's 0.45rem
   padding-block) renders a 14-19px tall link, under the 24px floor UX §8.3
   sets -- the debt the lookup tab-bar ledger (2026-09-17) left open for
   exactly these links, and which
   tests/e2e/test_pages.py::test_lookup_tap_targets_meet_the_24px_floor now
   measures on a real result page rather than on an empty form that had no
   such link to measure.

   inline-flex + min-block-size expands the hit area without pulling the link
   out of the surrounding text's flow, which a block-level fix (padding on
   the whole <td>) could not do: several links can share one cell -- an SOA
   row carries four, two on the primary nameserver and two on the mailbox
   domain -- and the floor has to be met by each link individually, not once
   per row. The RDAP link is in the same rule rather than a copy of it
   because it is the same 24px rule on the same page; it just is not a
   pivot. */
.ndx-pivot-link,
.ndx-rdap-link,
.ndx-rdap-link-wrap,
.ndx-related-link,
.ndx-related-link-wrap {
  display: inline-flex;
  align-items: center;
  min-block-size: 24px;
}

/* The inert variant of the registration control (2026-09-21, roadmap D33):
   a disabled <button> inside a tooltip wrapper, because a disabled control
   fires no pointer events and Bootstrap's documented answer is to put the
   tooltip on a wrapper around it. Its one surface was `/try/`, dropped
   2026-09-27; the presenter still supports `inert=True` and these rules are
   what it needs when a page uses it again. Both boxes have to clear 24px, not just
   the outer one -- tests/e2e/test_pages.py measures every <button> on the
   page individually, and a 24px wrapper around a 14px button is still a
   14px button. */
.ndx-rdap-link-wrap > .ndx-rdap-link,
.ndx-related-link-wrap > .ndx-related-link {
  min-block-size: 24px;
  min-inline-size: 24px;
}

/* The raw registry JSON inside the registration panel's <details>
   (components/registration_panel.html). `pre` is already one of
   tests/e2e/test_pages.py's _SCROLL_CONTAINERS, so a long line scrolls
   inside this box instead of pushing the page sideways at 375 -- but the
   wrapping rules below mean it almost never has to: RDAP payloads carry
   long unbroken URLs and handles, and `anywhere` is what breaks those.
   The block-size cap keeps a several-KB answer from burying the rest of
   the result page under it.

   No reduced-motion rule is needed for the collapse this sits inside:
   Bootstrap's own `@media (prefers-reduced-motion: reduce)` already
   disables the .collapsing transition. */
.ndx-registration-raw {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-block-size: 24rem;
  overflow: auto;
}

/* The <summary> of that <details> is a tap target the same 24px rule binds
   (UX §8.3) -- .ndx-sample's own rule sets only type and colour. */
details.ndx-sample > summary {
  display: list-item;
  min-block-size: 24px;
  padding-block: 0.25rem;
}

/* Only the pivot links need horizontal separation: .ndx-history-link and
   .ndx-rdap-link each sit alone, while two pivots sit back to back inside
   one cell and without this their expanded boxes visually touch. */
.ndx-pivot-link {
  margin-inline-end: 0.15rem;
}

/* The Last observed date as the link to a name's Observation history
   (components/observations_link.html, 2026-10-11). Inline-block with a 1.5rem
   line box so the date is a 24px tap target on a phone card, like the other
   in-table links; the tooltip carries the meaning and the price. */
.ndx-observed-link {
  display: inline-block;
  line-height: 1.5rem;
  min-height: 1.5rem;
}

/* A value that left the current set, in the Observation history table: muted,
   with the plain "no longer observed" words beside the dates (colour is never
   the only signal). */
.ndx-observations__row--past > th,
.ndx-observations__row--past > td {
  color: var(--bs-secondary-color);
}
.ndx-observations__past {
  font-style: italic;
}

/* The record-type explainer block under a DNS result table
   (components/dns_record_explainers.html, 2026-09-18). A spacing hook and
   nothing else: the eyebrow and the list already carry their look from
   .ndx-eyebrow and Bootstrap's .small/.text-body-secondary, so this rule owns
   only the gap that separates prose ABOUT the table from the table itself.
   Without it the first sentence sits directly against the last row and reads
   as another row of it. The list keeps its own indent -- these are several
   separate statements, and a flush-left run of them reads as one paragraph
   broken at odd places. */
.ndx-record-explainers {
  margin-block-start: 1rem;
}

/* The card's timestamp column. `.ndx-col-data` (mono + `overflow-wrap:
   anywhere`) is right for the query beside it -- a long TXT substring has to
   be allowed to break mid-token rather than widen the table -- and wrong for
   a timestamp, which has no natural break point: at 375 it broke
   "2026-09-17 17:44" across three lines and tripled every row's height.
   Verified in a browser at 375 on 2026-09-17; the fix is to opt this one
   column back out. The table sits in `.table-responsive`, so the extra width
   scrolls inside the card rather than pushing the page sideways. */
.ndx-history-when {
  overflow-wrap: normal;
  white-space: nowrap;
}

/* ==========================================================================
   Misc utilities that predate the design system and still have call sites
   ========================================================================== */



.js-copy.ndx-copied::after {
  content: "Copied";
  margin-left: 0.35rem;
  font-size: 0.8em;
  color: var(--ndx-accent-deep);
}

/* ==========================================================================
   Motion: only the copy confirmation fades; the wordmark is static.
   The animation lives below, inside a prefers-reduced-motion: no-preference guard --
   that polarity is deliberate: an unknown or unsupported preference gets the
   still version. ndxer/core/tests/test_css_rules.py fails if any
   `animation:` declaration in this file sits outside such a block. Adding a third
   animation is a design change, not an implementation detail.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  .js-copy.ndx-copied::after {
    animation: ndx-copied-fade 2s ease-out forwards;
  }


}

@keyframes ndx-copied-fade {
  0% {
    opacity: 1;
  }

  80% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}



/* ==========================================================================
   /top/ charts (ndxer/templates/top/top.html)

   NOT from docs/product/reviews/2026-08-22-brand/ux-design-system.md -- that
   document predates this page and describes no chart component. Added here
   2026-09-16 with the /top/ page; if the design system is revised, fold this
   in rather than letting the two drift.

   A chart on this page is always an ADDITION to a table that carries the same
   numbers, so this block only has to make the canvas legible and keep it
   inside its container at 375px. Chart.js draws with
   `maintainAspectRatio: false`, which means it fills whatever box it is given
   -- hence the explicit block size here rather than on the element.
   ========================================================================== */
.ndx-chart {
  margin-block: 0 1.75rem;
  /* Chart.js measures its canvas against the parent's content box. Without a
     bounded inline-size a canvas grows on every resize event (Chart.js issue
     #4156, the classic "chart keeps growing"), and on this page it would push
     the document sideways, which tests/e2e/test_pages.py fails the whole page
     for. */
  inline-size: 100%;
  max-inline-size: 760px;
}

/* The sized box is a separate element from the <figure> so the caption sits
   in NORMAL FLOW under it. An earlier draft gave the figure a fixed height
   and absolutely positioned the caption into the gap below; that put the
   caption over the next figure whenever a caption wrapped to two lines,
   which it does at 375px. */
.ndx-chart__box {
  position: relative;
  block-size: 260px;
}

.ndx-chart__box > canvas {
  max-inline-size: 100%;
}

.ndx-chart > figcaption {
  margin-block-start: 0.5rem;
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink-muted);
}

/* At phone width the plot gets more vertical room instead of squeezing twelve
   horizontal bars into 260px. */
@media (width <= 575.98px) {
  .ndx-chart__box {
    block-size: 320px;
  }
}

/* The corpus trend spans very different totals. Its log axis needs room
   for all three lines and readable ticks at both desktop and phone widths. */
.ndx-chart--corpus-trend .ndx-chart__box {
  block-size: 380px;
}

/* Approved compact homepage: the preview and copy share the first row.
   No fabricated chart is carried over from the Figma sketch: real totals
   below still come from the existing timestamped snapshot. */
.ndx-wordmark__mark {
  margin-inline-start: 0.3em;
  font-family: var(--ndx-sans);
  font-size: 0.85em;
  color: var(--ndx-accent-deep);
}

.ndx-footer .ndx-wordmark__mark {
  color: var(--ndx-accent);
}

.ndx-hero__layout {
  display: grid;
  gap: 2rem;
  align-items: center;
}

.ndx-hero__layout > * {
  min-inline-size: 0;
}

.ndx-hero__layout .ndx-term--hero {
  margin: 0;
  inline-size: 100%;
}

.ndx-preview {
  padding: 1.25rem;
}

.ndx-preview dl {
  margin: 1.25rem 0;
}

.ndx-preview dl > div {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  justify-content: space-between;
  padding-block: 0.65rem;
  border-block-end: 1px solid var(--ndx-border);
}

.ndx-preview dt,
.ndx-preview dd {
  margin: 0;
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-small);
  overflow-wrap: anywhere;
}

/* Owner-supplied editorial art (2026-10-04): decorative, never a data
   figure. Warm contained surfaces preserve the original image backgrounds. */
.ndx-insights-invite {
  display: grid;
  align-items: center;
  margin-block-end: 2rem;
  background: #fffcf7;
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
  overflow: hidden;
}

.ndx-insights-invite__copy {
  padding: 2rem;
}

.ndx-insights-invite__art {
  display: block;
  inline-size: 100%;
  block-size: auto;
  /* Match the paper tone and soften only the textured outer edge. */
  mask-image: linear-gradient(to right, transparent, #000 4%, #000 96%, transparent),
    linear-gradient(to bottom, transparent, #000 4%, #000 96%, transparent);
  mask-composite: intersect;
}

@media (min-width: 768px) {
  .ndx-insights-invite {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  .ndx-insights-invite__art {
    inline-size: auto;
    max-inline-size: 100%;
    max-block-size: 280px;
    justify-self: center;
  }
}

@media (min-width: 992px) {
  .ndx-hero__layout {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
    gap: 3rem;
  }
}

/* ==========================================================================
   /app/lookup/related/ figures (2026-09-26 visual layer; D39 explorer)

   Two SVG figures `ndxer/static/js/related.js` builds over data the page
   already renders in words: the GRAPH EXPLORER above the disclosure, and the
   seed's own rarity strip inside it. Read that file's header before editing
   either; this section holds only the colours, the box and the node list's
   six shape glyphs.

   COLOUR IS THE CLASS, AND NOTHING ELSE (owner, 2026-10-10: "align the colors
   in the graph too"). A node's stroke is its class colour, read from the
   `--ndx-class-*` tokens (subject, review, infrastructure, dismissed) --
   the same four colours the Class column, the facet and the tabs use. The
   five old signal-family hues (#2a78d6, #008300, #eda100 and two more) are
   GONE: they read as Infrastructure, Subject and To review, so a reader
   could not tell a family from a class.

   SHAPE IS THE NODE TYPE, DASH IS THE SIGNAL FAMILY. A node keeps its shape
   (circle, square, capsule, diamond, triangle, hexagon, stacked squares).
   The five families (`messages.KIND_FAMILIES`) are told apart by the DASH
   PATTERN of their strands, drawn in muted ink, and the legend shows each
   pattern beside the family's name: verification solid, name service long
   dashes, mail authentication dots, mail routing dash-dot, contacts short
   dashes. No sixth colour family exists and none may be added: colour in
   the graph means class, shape means type, dash means family.

   The dash lengths below are one table used twice: `stroke-dasharray` on a
   strand and a `repeating-linear-gradient` on the 16x3 key, so the key is
   the strand. Mail authentication's dots are the one pattern that needs a
   round cap on the strand (`.ndx-relex__edge` already has it). */

:root {
  --ndx-relfam-dash-nameservice: 7 4;
  --ndx-relfam-dash-mailauth: 1 4;
  --ndx-relfam-dash-mailroute: 7 3 1 3;
  --ndx-relfam-dash-contacts: 3 3;
}

/* Strand dashes, by the family id the script stamps on each strand. A
   strand to an Infrastructure value keeps its own finer dotted pattern
   (`--uses` below, declared after this block), because that says "context,
   not a hop". */
.ndx-relex__edge[data-family="nameservice"] {
  stroke-dasharray: var(--ndx-relfam-dash-nameservice);
}

.ndx-relex__edge[data-family="mailauth"] {
  stroke-dasharray: var(--ndx-relfam-dash-mailauth);
}

.ndx-relex__edge[data-family="mailroute"] {
  stroke-dasharray: var(--ndx-relfam-dash-mailroute);
}

.ndx-relex__edge[data-family="contacts"] {
  stroke-dasharray: var(--ndx-relfam-dash-contacts);
}

/* A node's class colour: `--ndx-relex-c`, set by the class the server put on
   the node (`data-class`), with the two server booleans as a fallback for a
   value node that carries no class id. Later rules win, so the explicit
   class id is last. Subject is the default. */
.ndx-relex__node {
  --ndx-relex-c: var(--ndx-class-subject);
}

.ndx-relex__node.is-unreviewed {
  --ndx-relex-c: var(--ndx-class-review);
}

.ndx-relex__node.is-uses {
  --ndx-relex-c: var(--ndx-class-infrastructure);
}

.ndx-relex__node[data-class="subject"] {
  --ndx-relex-c: var(--ndx-class-subject);
}

.ndx-relex__node[data-class="review"] {
  --ndx-relex-c: var(--ndx-class-review);
}

.ndx-relex__node[data-class="infrastructure"] {
  --ndx-relex-c: var(--ndx-class-infrastructure);
}

.ndx-relex__node[data-class="dismissed"] {
  --ndx-relex-c: var(--ndx-class-dismissed);
}

.ndx-relex {
  /* The frame's two heights (graph navigation, 2026-10-03). At 1440x900 the
     default is 576px and leaves page above and below it on screen; the
     taller view is the reader's choice. */
  --ndx-relex-frame-h: clamp(400px, 64vh, 600px);
  --ndx-relex-frame-h-tall: clamp(560px, calc(100vh - 7rem), 960px);
  /* The zoom buttons' size: 32px for a mouse, 44px under a coarse pointer. */
  --ndx-relex-ctl: 32px;

  inline-size: 100%;
  max-inline-size: 880px;
  margin-block: 0 1.25rem;
}

/* A phone keeps page visible above and below the frame at its default
   height, so one-finger scrolling past the picture always has somewhere to
   go (`svh`, so the collapsing URL bar does not resize it mid-scroll). */
@media (max-width: 767.98px) {
  .ndx-relex {
    --ndx-relex-frame-h: clamp(300px, 58svh, 440px);
    --ndx-relex-frame-h-tall: clamp(400px, 78svh, 640px);
    /* A narrow screen is almost always a finger: 44px whatever the
       pointer query reports (headless and some browsers report "fine"). */
    --ndx-relex-ctl: 44px;
  }
}

@media (pointer: coarse) {
  .ndx-relex {
    --ndx-relex-ctl: 44px;
  }

  /* The keys line describes a keyboard; a touch screen has the buttons. */
  .ndx-relex__keys {
    display: none;
  }
}

/* The toolbar is built by the script into the same mount the canvas is, so a
   reader with scripts off sees no controls that do nothing -- the node list
   below the figure is the unfiltered answer and it is complete. */
.ndx-relex__toolbar {
  /* Related page polish B: the action row below answers to the TOOLBAR's
     width, not the viewport's (`@container relexbar`), because the explorer
     sits in a column whose width the viewport does not decide. On the
     toolbar and not on `.ndx-relex`: a container gets layout containment,
     which would make the figure the containing block of the frame's
     fixed-position full-screen fallback. */
  container: relexbar / inline-size;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem 1.25rem;
  margin-block-end: 0.75rem;
}

.ndx-relex__find {
  display: flex;
  flex: 1 1 12rem;
  flex-direction: column;
  gap: 0.2rem;
  min-inline-size: 0;
}

.ndx-relex__find-label,
.ndx-relex__find-count {
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink-muted);
}

/* The field and its Clear on one line (related page polish B). */
.ndx-relex__find-row {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}

.ndx-relex__find-row > input {
  flex: 1 1 auto;
  min-inline-size: 0;
}

/* No filter checkboxes in this toolbar since the polish round (2026-10-02):
   the facet rail above the picture is the one filter control. */

/* The node actions plus the "More" menu (related page polish B, 2026-10-03):
   their own line, ONE row at every width, and the same height whatever is
   selected -- selecting a node puts its price on Expand, Subdomains and
   Addresses ("Expand · up to 8 units"), and a row that wrapped or grew then
   pushed the picture down under the pointer between the two clicks of a
   double-click (the e2e double-click test caught it). Wide (the toolbar at
   44rem and up) the six priced buttons fit one line with room to spare;
   narrower, the row holds only Expand, Hide, Undo and More (the
   `.ndx-relex__wide` buttons move into the menu as `[data-narrow]` items),
   every button 44px tall, and Expand alone may shrink and wrap its label to
   a second line inside that height. `tests/e2e/test_related_polish.py`
   holds it to one row and no overflow at 375 with a domain selected. */
.ndx-relex__actions {
  display: flex;
  flex: 1 1 100%;
  flex-wrap: nowrap;
  gap: 0.4rem;
  align-items: stretch;
  min-inline-size: 0;
}

.ndx-relex__actions > .btn {
  white-space: nowrap;
}

/* A disabled action is `aria-disabled`, not `disabled` (it stays focusable
   and says why), so Bootstrap's `:disabled` look is restated here. */
.ndx-relex__actions > .btn[aria-disabled="true"] {
  cursor: default;
  opacity: 0.65;
}


.ndx-relex__more {
  position: relative;
  display: flex;
  flex: none;
}

@container relexbar (max-width: 43.99rem) {
  .ndx-relex__wide {
    display: none;
  }

  .ndx-relex__actions > .btn,
  .ndx-relex__more-button {
    min-block-size: 2.75rem;
  }

  .ndx-relex__actions > .btn:first-child {
    flex: 0 1 auto;
    min-inline-size: 0;
    line-height: 1.15;
    white-space: normal;
  }
}

@container relexbar (min-width: 44rem) {
  .ndx-relex__menu [data-narrow] {
    display: none;
  }
}

/* Opens under its button, right-aligned to it, so at 375 -- where More ends
   the row -- it grows leftwards into the page and never past its edge. */
.ndx-relex__menu {
  position: absolute;
  inset-block-start: calc(100% + 0.25rem);
  inset-inline-end: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  min-inline-size: 13rem;
  max-inline-size: calc(100vw - 2rem);
  padding-block: 0.25rem;
  background: var(--ndx-surface);
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
  box-shadow: var(--ndx-shadow-pop);
}

.ndx-relex__menu[hidden] {
  display: none;
}

.ndx-relex__menu-item {
  min-block-size: 32px;
  padding: 0.3rem 0.75rem;
  font-size: var(--ndx-fs-small);
  color: var(--ndx-ink);
  text-align: start;
  background: none;
  border: 0;
}

.ndx-relex__menu-item:hover,
.ndx-relex__menu-item:focus-visible {
  background: var(--ndx-sunken);
}

/* The house focus ring (`:focus-visible` above), drawn inside the item so the
   menu's own edge never clips it. */
.ndx-relex__menu-item:focus-visible {
  outline-offset: -2px;
}

.ndx-relex__menu-item[aria-disabled="true"] {
  color: var(--ndx-ink-muted);
  cursor: default;
}

/* A toggle shows its state as a check drawn from two borders before the
   label; the state itself is `aria-checked`, which is what a screen reader
   reads. Drawn, not a glyph in `content`: generated text joins the item's
   accessible name (a check glyph before "Taller view"), which the e2e
   locators caught. */
.ndx-relex__menu-item[role="menuitemcheckbox"]::before {
  display: inline-block;
  inline-size: 0.35em;
  block-size: 0.7em;
  margin-inline: 0.2em 0.65em;
  vertical-align: 0.05em;
  content: "";
  border: solid transparent;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.ndx-relex__menu-item[aria-checked="true"]::before {
  border-color: currentcolor;
}

.ndx-relex__menu-sep {
  margin-block: 0.25rem;
  border-block-start: 1px solid var(--ndx-border);
}


/* The FRAME the explorer is navigated inside (2026-10-03). The tooltip, the
   zoom buttons, the readout, the locator and the hint are all positioned
   against it and clipped by it, so a figure can never be the thing that
   pushes the document sideways (tests/e2e/test_pages.py fails a page that
   scrolls horizontally). Focusable (`tabindex=0 role=group`) so the keys
   work; the house focus ring below. */
.ndx-relex__box {
  position: relative;
  overflow: hidden;
  block-size: var(--ndx-relex-frame-h);
  background: var(--ndx-surface);
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
}

.ndx-relex__box.is-tall {
  block-size: var(--ndx-relex-frame-h-tall);
}

/* Clicked into, pointer still inside: a plain wheel now zooms. The stronger
   border is the only sign, and the hint says how to get here. */
.ndx-relex__box.is-engaged {
  border-color: var(--ndx-border-strong);
}

/* Space held over a focused frame: the press pans from anywhere. */
.ndx-relex__box.is-panning,
.ndx-relex__box.is-panning .ndx-relex__hit {
  cursor: grab;
}

/* Full screen (graph round, 2026-10-03). The Fullscreen API sizes the frame
   itself; where it is missing or refused the frame becomes a fixed overlay
   over the page instead, and the page under it stops scrolling. Either way
   the camera refits to the new box (related.js `onFullscreen`). */
.ndx-relex__box:fullscreen {
  border: 0;
  border-radius: 0;
}

.ndx-relex__box.is-fullscreen:not(:fullscreen) {
  position: fixed;
  inset: 0;
  z-index: 1080;
  block-size: 100dvh;
  inline-size: 100vw;
  border: 0;
  border-radius: 0;
}

.ndx-relex-locked,
.ndx-relex-locked body {
  overflow: hidden;
}

.ndx-relex__box:focus-visible {
  outline: 2px solid var(--ndx-ink);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px var(--ndx-accent-soft);
}

/* The explorer's SVG fills the frame and its viewBox IS the frame's pixel
   box (related.js `measureFrame`), so one unit is one CSS pixel and marks
   stay screen-constant at every zoom.

   `touch-action: pan-y` is the phone's no-scroll-trap rule: one finger
   always scrolls the page and a tap still selects, while d3-zoom accepts a
   touch only once two fingers are down. */
.ndx-relex__svg {
  display: block;
  inline-size: 100%;
  block-size: 100%;
  touch-action: pan-y;
  cursor: grab;
}

.ndx-relex__svg:active {
  cursor: grabbing;
}

/* The strip keeps aspect sizing: no width/height attributes and no fixed
   block-size, its viewBox's aspect ratio sizes it. Unlike `.ndx-chart__box`,
   nothing here needs a bounded box -- that constraint is Chart.js's. */
.ndx-relstrip__svg {
  display: block;
  inline-size: 100%;
  max-inline-size: 100%;
  block-size: auto;
}

/* `--ndx-relex-strand` is set on the SVG by related.js from the zoom:
   0.35 at a distance, rising to 0.6 by 2.7x. A strand touching a quiet domain is
   a third of that, so at 1x the many weak strands are a faint texture under
   the strong ones. Pointing at or selecting a node lifts its strands to 1
   (`[data-lit]` below). */
.ndx-relex__edge {
  fill: none;
  stroke-linecap: round;
  stroke-opacity: var(--ndx-relex-strand, 0.8);
}

.ndx-relex__edge.is-quiet {
  stroke-opacity: calc(var(--ndx-relex-strand, 0.8) * 0.35);
}

.ndx-relex__svg[data-lit] .ndx-relex__edge.is-active {
  stroke-opacity: 1;
}

/* A quiet domain (level of detail): its own mark, scaled down by related.js
   to a dot that grows with the zoom, without band ticks or the expand dot,
   which at that size are noise. It becomes a full mark once the camera
   gives it room, or the moment the reader points at, finds, selects, holds
   or opens it. */
.ndx-relex__node.is-quiet .ndx-relex__unreviewed,
.ndx-relex__node.is-quiet .ndx-relex__dot {
  display: none;
}

/* The four zoom buttons, top-right inside the frame. */
.ndx-relex__zoom {
  position: absolute;
  inset-block-start: 8px;
  inset-inline-end: 8px;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.ndx-relex__ctl.btn {
  min-inline-size: var(--ndx-relex-ctl);
  min-block-size: var(--ndx-relex-ctl);
  padding: 0 0.4rem;
  font-family: var(--ndx-mono);
  line-height: 1;
  background-color: var(--ndx-surface);
}

/* The readout, bottom-left: counts and the zoom factor, one line. */
.ndx-relex__hud {
  position: absolute;
  inset-block-end: 8px;
  inset-inline-start: 8px;
  z-index: 1;
  max-inline-size: calc(100% - 16px);
  padding: 0.1rem 0.4rem;
  overflow: hidden;
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink-muted);
  white-space: nowrap;
  text-overflow: ellipsis;
  pointer-events: none;
  background: color-mix(in srgb, var(--ndx-surface) 92%, transparent);
  border-radius: var(--ndx-radius);
}

/* On a phone one line cannot hold the counts and the zoom factor, and the
   factor is the number a reader navigating needs, so the readout may take a
   second line there instead of clipping it. */
@media (max-width: 767.98px) {
  .ndx-relex__hud {
    white-space: normal;
  }
}

/* "You are here": the picture's outline, the shared values, the seed and
   the viewport rectangle. Above 1.5x on a frame 768px or wider only. */
.ndx-relex__locator {
  position: absolute;
  inset-block-end: 8px;
  inset-inline-end: 8px;
  z-index: 1;
  inline-size: 120px;
  block-size: 80px;
  cursor: pointer;
  background: color-mix(in srgb, var(--ndx-surface) 92%, transparent);
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
}

.ndx-relex__locator[hidden] {
  display: none;
}

.ndx-relex__locator-frame {
  fill: none;
  stroke: var(--ndx-border);
  stroke-width: 1;
}

.ndx-relex__locator-seed {
  fill: var(--ndx-ink);
}

.ndx-relex__locator-view {
  fill: none;
  stroke: var(--ndx-ink);
  stroke-width: 1.5;
}

/* "Ctrl or ⌘ and scroll to zoom, or click the picture first", for a
   moment, when a plain wheel scrolled the page past the frame. */
.ndx-relex__hint {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  z-index: 3;
  max-inline-size: calc(100% - 32px);
  padding: 0.4rem 0.75rem;
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink);
  text-align: center;
  pointer-events: none;
  visibility: hidden;
  background-color: var(--ndx-surface);
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
  box-shadow: var(--ndx-shadow-pop);
  opacity: 0;
  transform: translate(-50%, -50%);
}

.ndx-relex__hint.is-shown {
  visibility: visible;
  opacity: 1;
}

.ndx-relex__keys {
  margin-block: 0.25rem 0;
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink-muted);
}

/* The class readout and the hidden control under the frame (D42 §9). */
.ndx-relex__status {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  align-items: center;
  margin-block: 0.25rem 0;
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink-muted);
}

.ndx-relex__hidden-toggle.btn {
  min-block-size: 24px;
  padding: 0 0.25rem;
  font-size: var(--ndx-fs-meta);
}

.ndx-relex__hidden {
  padding: 0.5rem 0.75rem;
  margin-block: 0.25rem 0;
  background: var(--ndx-paper);
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
}

.ndx-relex__hidden-list li {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
  align-items: center;
}

.ndx-relnodes__group-toggle.btn {
  padding: 0;
  font-size: inherit;
  text-align: start;
}

/* Surface fill on every node, so a strand passing behind one does not read
   through it. The CLASS colour is on the STROKE (`--ndx-relex-c`, set per
   node above), where a 2.5px line carries it. */
.ndx-relex__shape {
  fill: var(--ndx-surface);
  stroke: var(--ndx-relex-c, var(--ndx-class-subject));
}

/* The seed is ink, with a surface ring that keeps the converging strands off
   the node itself -- not a border drawn to separate it from the paper. */
.ndx-relex__shape--seed {
  fill: var(--ndx-ink);
  stroke: var(--ndx-surface);
  stroke-width: 3;
}

/* A value shared by more domains than its kind counts as evidence, and which
   NOBODY HAS BEEN ABLE TO NAME. Dashing is banned on grids and axes; on a node
   stroke it is the one place it means something, and what it means is "this
   one does not open". */
.ndx-relex__shape--common {
  fill: var(--ndx-paper);
  stroke: var(--ndx-border-strong);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

/* A value `ndxer.signal_annotations` NAMES a provider for (owner ruling
   2026-09-26). Inert like the one above and drawn unlike it, because the two
   are different findings: this one the page can tell you whose it is, and the
   node list beside the canvas prints that name and the provider's own
   sentence.

   NO SIXTH HUE. The five family hues are a measured all-pairs set on this
   surface (see the block above `--ndx-relfam-dash-*`) and adding a colour to mean
   "provider" would spend that measurement. The three channels used instead are
   all free: a SOLID stroke where `--common` dashes, the muted ink where
   `--common` takes the strong border, and 2px where `--common` takes 1.5 --
   so an annotated value reads as deliberate and named, an unnamed one as a
   faint dotted outline, and a scoreable one keeps its family hue and its
   expand dot. Neither inert node carries the dot at all, which is the
   difference a reader actually acts on. */
.ndx-relex__shape--shared {
  fill: var(--ndx-paper);
  stroke: var(--ndx-class-infrastructure);
  stroke-width: 2;
}

.ndx-relex__halo {
  fill: var(--ndx-ink);
  opacity: 0.06;
}

.ndx-relex__halo-ring {
  fill: none;
  stroke: var(--ndx-border);
  stroke-width: 1;
}

/* The proposed class, drawn (roadmap D42 §9, graph round 2026-10-03; this
   replaces W1's arc ticks). Subject: the mark in full, nothing added. To
   review: the mark in full inside a DASHED ring -- "not yet reviewed" --
   never a hue, because the five hues mean signal family page-wide and must
   not mean two things. Infrastructure: a small muted "uses" mark below. */
.ndx-relex__unreviewed {
  fill: none;
  stroke: var(--ndx-class-review);
  stroke-width: 1.5;
  stroke-dasharray: 3 3;
}

/* Infrastructure (D42 §3): context, never a hop. A 4px muted mark that never
   carries the expand dot, and thin dotted strands to it, so a provider's
   value is visibly something the subject USES rather than part of it. Not
   the `--shared` treatment (a named provider's value at full size): the
   class is the reader-facing fact, and it is smaller on purpose. */
.ndx-relex__shape--uses {
  fill: var(--ndx-paper);
  stroke: var(--ndx-class-infrastructure);
  stroke-width: 1.5;
}

.ndx-relex__edge.ndx-relex__edge--uses {
  stroke-dasharray: 2 3;
}

/* A group node (D42 §9): the kind's own shape twice, the back copy offset,
   like a record set's two squares -- "several of these". Click, Enter or the
   node list's button opens it in place. */
.ndx-relex__shape--group {
  stroke-width: 2.5;
}

.ndx-relex__shape--group-back {
  fill: var(--ndx-surface);
  stroke: var(--ndx-border-strong);
  stroke-width: 1.5;
}

/* The expand affordance. A 1.5px surface ring so it survives over a strand. */
.ndx-relex__dot {
  fill: var(--ndx-ink);
  stroke: var(--ndx-surface);
  stroke-width: 1.5;
}

.ndx-relex__opened {
  fill: none;
  stroke: var(--ndx-border);
  stroke-width: 1;
}

/* Three static dots while an answer is on its way. Not a spinner: there is no
   animation in this feature in either motion mode. */
.ndx-relex__waiting {
  fill: var(--ndx-ink-muted);
}

.ndx-relex__label {
  paint-order: stroke fill;
  font-family: var(--ndx-mono);
  font-size: 12px;
  fill: var(--ndx-ink);
  stroke: var(--ndx-surface);
  stroke-width: 3px;
  stroke-linejoin: round;
  dominant-baseline: middle;
}

/* Painted marks never take the pointer; the invisible circle over each node
   does, which is a 28px effective target at any width. */
.ndx-relex__edges,
.ndx-relex__nodes,
.ndx-relex__labels {
  pointer-events: none;
}

.ndx-relex__hit {
  fill: transparent;
  cursor: pointer;
  pointer-events: all;
}

.ndx-relex__svg[data-active] .ndx-relex__mark {
  opacity: 0.22;
}

.ndx-relex__svg[data-active] .ndx-relex__mark.is-active {
  opacity: 1;
}

/* The house "found" wash at 1.50:1 -- it can never read as --ndx-danger, and
   the same class marks the matching row in the node list, so the state is
   never carried by colour alone. */
.ndx-relex__node.is-found .ndx-relex__shape {
  fill: var(--ndx-select-bg);
}

.ndx-relex__node.is-selected .ndx-relex__shape {
  stroke-width: 3.5;
}

/* Held in place by the reader: a square cap on the stroke ends, which reads
   as "fixed" beside the round caps everything else uses, plus the word in the
   node list. */
.ndx-relex__node.is-pinned .ndx-relex__unreviewed {
  stroke-dasharray: 1 3;
}

.ndx-relex__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin-block: 0.5rem 0;
  padding: 0;
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink);
  list-style: none;
}

.ndx-relex__legend > li {
  display: flex;
  gap: 0.4rem;
  align-items: center;
}

.ndx-relex__legend-title {
  font-family: var(--ndx-mono);
  color: var(--ndx-ink-muted);
}

/* These are LINE marks in the graph, so the family key is a line and not a
   swatch. Text never wears a family hue -- the key beside it carries the
   identity (UX §5.1, and the palette note above). */
.ndx-relex__key {
  display: inline-block;
  flex: none;
  inline-size: 16px;
  block-size: 3px;
  border-radius: 2px;
  background-color: var(--ndx-ink-muted);
}

/* The class dot beside a class word (readout, tooltip, node legend). The
   colour is the class token; the word beside it stays ink, so text never
   wears a class hue. */
.ndx-relex__classdot {
  display: inline-block;
  flex: none;
  inline-size: 0.6rem;
  block-size: 0.6rem;
  margin-inline-end: 0.3rem;
  border-radius: 50%;
  background-color: var(--ndx-class-subject);
}

.ndx-relex__classdot[data-class="review"] {
  background-color: var(--ndx-class-review);
}

.ndx-relex__classdot[data-class="infrastructure"] {
  background-color: var(--ndx-class-infrastructure);
}

.ndx-relex__classdot[data-class="dismissed"] {
  background-color: var(--ndx-class-dismissed);
}

.ndx-relex__class {
  white-space: nowrap;
}

.ndx-relex__ctl .bi {
  pointer-events: none;
}

.ndx-relex > figcaption,
.ndx-relstrip > figcaption {
  margin-block-start: 0.5rem;
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink-muted);
}

.ndx-relex__tip {
  position: absolute;
  z-index: 3;
  max-inline-size: 22rem;
  padding: 0.5rem 0.625rem;
  font-size: var(--ndx-fs-meta);
  background-color: var(--ndx-surface);
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
  box-shadow: var(--ndx-shadow-pop);
  pointer-events: none;
}

.ndx-relex__tip-label {
  margin: 0;
  overflow-wrap: anywhere;
  font-family: var(--ndx-mono);
  font-weight: 600;
  color: var(--ndx-ink);
}

.ndx-relex__tip-meta {
  margin: 0 0 0.25rem;
  color: var(--ndx-ink-muted);
}

.ndx-relex__tip-note,
.ndx-relex__tip-cost {
  margin: 0.25rem 0 0;
  color: var(--ndx-ink-muted);
}

.ndx-relex__tip-cost {
  font-family: var(--ndx-mono);
}

.ndx-relex__tip-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.ndx-relex__tip-list > li {
  display: flex;
  gap: 0.4rem;
  align-items: baseline;
}

.ndx-relex__tip-text {
  overflow-wrap: anywhere;
  color: var(--ndx-ink);
}

/* Below 768px the readout is a fixed strip under the figure rather than a
   floating tooltip: a tooltip pinned to the touched node is under the finger
   that summoned it. One line, reserved height, so touching a node does not
   reflow the page under the reader. */
.ndx-relex__readout {
  min-block-size: 1.5rem;
  margin-block-start: 0.25rem;
  overflow-wrap: anywhere;
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink);
}

.ndx-relex__units {
  font-family: var(--ndx-mono);
}

/* --- the node list: the explorer's keyboard surface ---------------------- */

.ndx-relnodes__list {
  margin: 0;
  padding: 0;
}

.ndx-relnodes__item {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-items: center;
  min-block-size: 24px;
  padding-block: 0.15rem;
  overflow-wrap: anywhere;
}

.ndx-relnodes__item.is-dimmed {
  opacity: 0.45;
}

.ndx-relnodes__item.is-found {
  background-color: var(--ndx-select-bg);
}

/* `__action`: the per-domain "Subdomains" / "Addresses" controls (the subject
   graph, 2026-10-03), drawn exactly like Expand but a class of their own so
   nothing that looks for a node's Expand control finds them. */
.ndx-relnodes__expand,
.ndx-relnodes__action {
  padding: 0.1rem 0.4rem;
  font-size: var(--ndx-fs-meta);
  font-family: var(--ndx-mono);
  color: var(--ndx-ink);
  background: none;
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
  text-decoration: none;
}

.ndx-relnodes__note,
.ndx-relnodes__state {
  font-size: var(--ndx-fs-meta);
}

/* --- "This domain uses": the shared-infrastructure panel ----------------
   (owner ruling 2026-09-26, roadmap D39 amendment.)

   A list and not a card: it sits inside the result card, above the picture,
   and a second bordered box inside the first one would read as a second
   answer. The category is the emphasis because it is what a reader scans for
   ("who does their mail?"); the provider name is the answer beside it, and the
   sentence under both is the provider's own. Nothing here uses colour to mean
   anything -- the panel is words, which is also why it is the relief channel
   for the two canvas hues that sit below 3:1 on white. */
.ndx-relprov__list {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin: 0;
  padding: 0;
}

.ndx-relprov__item {
  padding-inline-start: 0.6rem;
  border-inline-start: 2px solid var(--ndx-border);
  overflow-wrap: anywhere;
}

.ndx-relprov__category {
  font-weight: 600;
}

.ndx-relprov__category::after {
  content: " ·";
  color: var(--ndx-ink-muted);
  font-weight: 400;
}

.ndx-relprov__provider {
  font-family: var(--ndx-mono);
}

.ndx-relprov__note {
  font-size: var(--ndx-fs-meta);
}

/* The provider's sentence beside a signal in the seed's own list and in an
   evidence cell. A block, so it wraps under the row rather than trailing off
   the end of a long key on a 375px screen. */
.ndx-relsignals__note {
  display: block;
  font-size: var(--ndx-fs-meta);
}

/* The 12px shape glyph beside each item: the same six shapes the canvas
   draws, in CSS rather than inline SVG so no template has to carry a path.
   `aria-hidden`, because the group heading above it says the type in words. */
.ndx-relnodes__glyph {
  display: inline-block;
  flex: none;
  inline-size: 12px;
  block-size: 12px;
  background-color: var(--ndx-surface);
  border: 2px solid var(--ndx-ink);
}

.ndx-relnodes__glyph--apex {
  border-radius: 50%;
}

/* A name under a domain (the subject graph, 2026-10-03): the domain's circle
   with a dot inside it, drawn as a radial gradient so it needs no element. */
.ndx-relnodes__glyph--subdomain {
  border-radius: 50%;
  background: radial-gradient(circle, var(--ndx-ink) 0 2px, var(--ndx-surface) 2.5px);
}

/* The canvas twin of that dot; its fill is the mark's own stroke colour. */
.ndx-relex__dot {
  pointer-events: none;
}

.ndx-relnodes__glyph--host {
  border-radius: 2px;
}

/* Two squares, the back one offset: the offset IS the encoding, because a set
   node carries a hash of a whole ordered list and there are no members to
   draw. The box-shadow is the back square. */
.ndx-relnodes__glyph--set {
  border-radius: 2px;
  box-shadow: 3px -3px 0 -1px var(--ndx-surface), 3px -3px 0 0 var(--ndx-ink);
}

.ndx-relnodes__glyph--mailbox {
  transform: rotate(45deg);
}

.ndx-relnodes__glyph--token {
  background-color: var(--ndx-ink);
  border: 0;
  clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
}

/* The seed's first label (`name_label`, code review 2026-10-03): a triangle. */
.ndx-relnodes__glyph--name {
  background-color: var(--ndx-ink);
  border: 0;
  clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
}

.ndx-relnodes__glyph--address {
  inline-size: 14px;
  block-size: 8px;
  border-radius: 5px;
}

.ndx-relstrip {
  inline-size: 100%;
  max-inline-size: 640px;
  margin-block: 0.75rem 0.25rem;
}

.ndx-relstrip__lane {
  font-family: var(--ndx-mono);
  font-size: 11px;
  fill: var(--ndx-ink-muted);
}

.ndx-relstrip__axis,
.ndx-relstrip__tick {
  stroke: var(--ndx-border);
  stroke-width: 1;
}

.ndx-relstrip__tickLabel,
.ndx-relstrip__more,
.ndx-relstrip__axisLabel {
  font-family: var(--ndx-mono);
  font-size: 10px;
  fill: var(--ndx-ink-muted);
}

.ndx-relstrip__dot--outline {
  stroke-width: 2;
}

/* Selecting a node scrolls to its table row and lights it for two seconds.
   Both the custom property and the background-color are set: Bootstrap 5.3's
   striped rows paint through `--bs-table-bg-type` on the cells, so setting
   only one of the two leaves every other row lit and this one not. The class
   keeps its `relgraph` name through the D39 rewrite: it is the TABLE's
   highlight, and the e2e test that drives it is about the table. */
tr.ndx-relgraph__row--lit {
  --bs-table-bg-type: var(--ndx-accent-soft);
}

tr.ndx-relgraph__row--lit > * {
  background-color: var(--ndx-accent-soft);
}

/* The only motion in the feature, and the polarity is the design system's
   (UX §8.2, enforced by ndxer/core/tests/test_css_rules.py): declared INSIDE
   a no-preference block, so an unknown or unsupported preference gets the
   still version. There is no entrance animation, no settling on first paint
   and no pulsing in either mode -- the layout is solved in a fixed
   synchronous tick budget before anything is drawn, and the only thing that
   may move is an expansion the reader asked for, inside a capped budget
   related.js enforces in JavaScript rather than in a keyframe. */
@media (prefers-reduced-motion: no-preference) {
  .ndx-relex__mark {
    transition: opacity 120ms ease;
  }

  .ndx-relex__hint {
    transition:
      opacity 160ms ease,
      visibility 160ms;
  }

  tr.ndx-relgraph__row--lit > * {
    transition: background-color 120ms ease;
  }
}

/* Shared signal facets, retained by Investigations (D43). */

/* --- the facet rail ----------------------------------------------------- */

.ndx-ws__rail > summary {
  cursor: pointer;
}

.ndx-ws__rail-help {
  margin-block: 0.4rem 0.75rem;
  font-size: var(--ndx-fs-meta);
}

.ndx-ws__facet {
  margin-block-end: 1rem;
  padding: 0;
  border: 0;
}

.ndx-ws__facet-legend {
  float: none;
  inline-size: auto;
  margin-block-end: 0.25rem;
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  color: var(--ndx-ink-muted);
}

/* 24px is the tap-target floor. The BAR gets a bounded track and the row
   packs to the start (`justify-content: start`), rather than the bar taking
   every pixel of slack: in a wide inline rail a `1fr` bar drew a count of 1
   as a 400px strip, which reads as a big number. A bar is a comparison
   between the rows of ONE group, so its length only has to be legible. The
   count keeps its own column, right-aligned and tabular so two rows' digits
   line up. */
.ndx-ws__facet-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) minmax(1.5rem, 4rem) 3ch;
  align-items: center;
  gap: 0.4rem;
  min-block-size: 24px;
  cursor: pointer;
}

.ndx-ws__facet-row:has(.ndx-ws__facet-key) {
  grid-template-columns: auto auto minmax(0, 1fr) minmax(1.5rem, 4rem) 3ch;
}

/* Selected reads three ways at once and never by colour alone: the checkbox
   is checked, the label goes semibold, and the bar fills with ink. */
.ndx-ws__facet-row.is-on .ndx-ws__facet-label {
  font-weight: 600;
  color: var(--ndx-ink);
}

.ndx-ws__facet-row.is-on .ndx-ws__facet-fill {
  background-color: var(--ndx-ink);
}

/* The label takes the slack and the bar is capped, rather than the other way
   round. Two measured reasons: in the 260px rail a fixed 6rem bar squeezed
   "Mail authentication" onto three lines, and in the wide inline rail a `1fr`
   bar drew a count of 1 as a 400px strip, which reads as a big number. A bar
   compares the rows of ONE group; it only has to be legible. */
.ndx-ws__facet-label {
  min-inline-size: 0;
  justify-self: start;
  font-size: var(--ndx-fs-small);
  /* NOT `overflow-wrap: anywhere`: in a 260px rail that broke "Mail
     authentication" mid-word across three lines. A long label wraps at its
     space like any other text; the rail is a list of words, not a table
     cell holding a hostname. */
  hyphens: none;
}

/* The 16x3 line key, on the signal-family group only. */
.ndx-ws__facet-key {
  display: inline-block;
  inline-size: 16px;
  block-size: 3px;
  border-radius: 2px;
  background-color: var(--ndx-ink-muted);
}

/* The family keys wear the strand's own dash, in muted ink (colour in the
   graph means class; see the figures block). One table with
   `--ndx-relfam-dash-*`, drawn as a gradient so the key is the strand. */
.ndx-ws__facet-key--nameservice,
.ndx-relex__key[data-family="nameservice"] {
  background: repeating-linear-gradient(to right, var(--ndx-ink-muted) 0 7px, transparent 7px 11px);
}

.ndx-ws__facet-key--mailauth,
.ndx-relex__key[data-family="mailauth"] {
  background: repeating-linear-gradient(to right, var(--ndx-ink-muted) 0 2px, transparent 2px 5px);
}

.ndx-ws__facet-key--mailroute,
.ndx-relex__key[data-family="mailroute"] {
  background: repeating-linear-gradient(
    to right,
    var(--ndx-ink-muted) 0 7px,
    transparent 7px 10px,
    var(--ndx-ink-muted) 10px 11px,
    transparent 11px 14px
  );
}

.ndx-ws__facet-key--contacts,
.ndx-relex__key[data-family="contacts"] {
  background: repeating-linear-gradient(to right, var(--ndx-ink-muted) 0 3px, transparent 3px 6px);
}

/* The bar is sized by the script as a share of the largest count in its own
   group. Zero width until then, so a page with no script shows the label and
   the number and no misleading full-width bar. */
.ndx-ws__facet-bar {
  display: block;
  justify-self: end;
  inline-size: 100%;
  max-inline-size: 4rem;
  block-size: 6px;
  border-radius: 3px;
  background-color: var(--ndx-sunken);
}

.ndx-ws__facet-fill {
  display: block;
  inline-size: 0;
  block-size: 6px;
  border-radius: 3px;
  background-color: var(--ndx-border-strong);
}

.ndx-ws__facet-count {
  font-family: var(--ndx-mono);
  font-size: var(--ndx-fs-meta);
  font-variant-numeric: tabular-nums;
  color: var(--ndx-ink-muted);
  text-align: end;
}

/* --- the section cards -------------------------------------------------- */

/* --- the polish round (owner decision 2026-10-02) ------------------------

   "no filters, too much depth, I don't see the subdomains". A facet now
   NARROWS the table and the node list (the `hidden` attribute, which
   Bootstrap's reboot makes win) and dims only the picture -- so the rule that
   dimmed a table row is gone. What is here is the rail's link rows, the one
   "Details" disclosure, the names teaser and the expand controls a table row
   and an evidence chip now carry. */

/* A rail row is a LINK on the related page and on the workspace's section 1:
   the page itself with one value toggled, which `facets.js` intercepts. It
   keeps the checkbox row's grid -- the box glyph takes the checkbox's column
   -- and reads as a control rather than as prose: ink, no underline. */
/* `a.` and the rail ancestor are specificity, not style: `.ndx-prose a:not(...)`
   underlines every link in the main column in accent orange, and a rail of
   eighteen underlined labels plus underlined counts read as a paragraph of
   links (1440 screenshot, 2026-10-02). Same specificity, declared later. */
.ndx-ws__rail a.ndx-ws__facet-link {
  color: var(--ndx-ink);
  text-decoration: none;
}

.ndx-ws__rail a.ndx-ws__facet-link:hover {
  background: none;
}

.ndx-ws__rail a.ndx-ws__facet-link:hover .ndx-ws__facet-label,
.ndx-ws__rail a.ndx-ws__facet-link:focus-visible .ndx-ws__facet-label {
  text-decoration: underline;
  text-decoration-color: var(--ndx-accent-deep);
}

/* At phone width the link rail flows its rows as compact chips -- box,
   label, count -- and drops the bars, which only compare rows within a group
   and need a column to do it. Stacked one per line the four groups pushed the
   graph two screens down (375 screenshot, 2026-10-02). The count stays: it is
   the fact; the bar was its picture. */
@media (max-width: 575.98px) {
  .ndx-ws__rail a.ndx-ws__facet-link {
    display: inline-flex;
    margin-inline-end: 0.9rem;
  }

  .ndx-ws__rail a.ndx-ws__facet-link .ndx-ws__facet-bar {
    display: none;
  }

  .ndx-ws__rail .ndx-ws__facet {
    margin-block-end: 0.5rem;
  }
}

/* The chosen state's third cue beside `aria-current` and the semibold label:
   a 12px box that fills with ink. Never colour alone -- the fill is a shape
   change on a bordered box. */
.ndx-ws__facet-box {
  display: inline-block;
  inline-size: 12px;
  block-size: 12px;
  border: 1.5px solid var(--ndx-border-strong);
  border-radius: 2px;
  background-color: var(--ndx-surface);
}

.ndx-ws__facet-link.is-on .ndx-ws__facet-box {
  border-color: var(--ndx-ink);
  background-color: var(--ndx-ink);
}

.ndx-ws__facet-help {
  margin-block: 0 0.35rem;
  font-size: var(--ndx-fs-meta);
}

/* The related page's rail sits above the picture, full width: its groups
   spread side by side at every width that can hold two of them, rather than
   only from 992px as the estate section's copy does. */
.ndx-relpage .ndx-ws__rail--inline .ndx-ws__facet-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0 1.5rem;
  align-items: start;
}

.ndx-relpage .ndx-ws__rail--inline {
  margin-block-end: 0.75rem;
}

/* The names teaser: one line, the link is the sentence. */
.ndx-relnames__link {
  font-family: var(--ndx-mono);
}

/* "Details: 15 nodes, 6 signals". The summary is a tap target the 24px rule
   binds like any other control. */
.ndx-reldetails > summary {
  min-block-size: 24px;
  cursor: pointer;
}

/* A table row's and an evidence chip's expand control: the node list's own
   control, set a little apart from the text it follows, and tall enough to
   clear the 24px tap-target floor on its own. */
.ndx-relexpand,
.ndx-relaction {
  display: inline-flex;
  align-items: center;
  min-block-size: 24px;
  margin-inline-start: 0.35rem;
  white-space: nowrap;
}

.ndx-relexpand__state {
  margin-inline-start: 0.35rem;
  font-size: var(--ndx-fs-meta);
}

/* The one-line "This domain uses": category, provider and the provider's own
   sentence on one wrapping line, a dash before the sentence. */
.ndx-relprov__note::before {
  content: "— ";
}

/* --- proposed classes (roadmap D42, 2026-10-03) ------------------------- */

/* The class pill (owner-locked 2026-10-10): ONE treatment for all four
   classes -- a dot, the class word, and the class tint -- so colour is never
   the only signal (WCAG 1.4.1). Markup:
   <span class="ndx-class ndx-class--subject"><span class="ndx-class__dot" aria-hidden="true"></span>Subject</span>
   The same element is the trigger of a row's "Move to" menu when it is a
   <button>. Supersedes the 2026-10-03 accent/sunken/dashed/struck variants. */
.ndx-class {
  --ndx-class-c: var(--ndx-class-dismissed);
  --ndx-class-t: var(--ndx-class-dismissed-text);
  --ndx-class-s: var(--ndx-class-dismissed-soft);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.65rem;
  border: 0;
  border-radius: 999px;
  font-weight: 500;
  font-size: var(--ndx-fs-meta, 0.85rem);
  line-height: 1.4;
  color: var(--ndx-class-t);
  background-color: var(--ndx-class-s);
  white-space: nowrap;
}

.ndx-class__dot {
  inline-size: 0.5rem;
  block-size: 0.5rem;
  border-radius: 50%;
  background-color: var(--ndx-class-c);
  flex: none;
}

.ndx-class--subject {
  --ndx-class-c: var(--ndx-class-subject);
  --ndx-class-t: var(--ndx-class-subject-text);
  --ndx-class-s: var(--ndx-class-subject-soft);
}

.ndx-class--review {
  --ndx-class-c: var(--ndx-class-review);
  --ndx-class-t: var(--ndx-class-review-text);
  --ndx-class-s: var(--ndx-class-review-soft);
}

.ndx-class--infrastructure {
  --ndx-class-c: var(--ndx-class-infrastructure);
  --ndx-class-t: var(--ndx-class-infrastructure-text);
  --ndx-class-s: var(--ndx-class-infrastructure-soft);
}

button.ndx-class {
  min-block-size: 2.5rem;
  min-inline-size: 7.5rem;
  justify-content: space-between;
  cursor: pointer;
}

button.ndx-class:hover {
  filter: brightness(0.96);
}

button.ndx-class:focus-visible,
.ndx-ib:focus-visible {
  outline: 2px solid var(--ndx-accent);
  outline-offset: 1px;
}

/* Icon buttons (owner-locked 2026-10-10): borderless, muted until their row
   is hovered or focused, the same fixed slot on every row of every tab.
   Inapplicable = aria-disabled (still focusable, tooltip says why), never
   hidden and never the `disabled` attribute. A metered action carries its
   price as .ndx-ib__cost, visible before the press (D38). Markup:
   components/icon_button.html. */
.ndx-ib {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  padding: 0;
  border: 0;
  border-radius: var(--ndx-radius);
  background: transparent;
  color: var(--ndx-ink-muted);
  text-decoration: none;
}

.ndx-ib .bi {
  font-size: 1.1rem;
  line-height: 1;
}

tr:hover .ndx-ib,
tr:focus-within .ndx-ib,
.ndx-ib:hover {
  color: var(--ndx-ink);
}

.ndx-ib:hover {
  background-color: var(--ndx-sunken);
}

.ndx-ib[aria-disabled="true"] {
  color: var(--ndx-border) !important;
  background: transparent !important;
  cursor: not-allowed;
}

.ndx-ib--accept:not([aria-disabled="true"]) {
  color: var(--ndx-class-subject-text);
}

.ndx-ib--accept:not([aria-disabled="true"]):hover {
  background-color: var(--ndx-class-subject-soft);
}

.ndx-tab-bar__link .bi {
  margin-inline-end: 0.35rem;
}

.ndx-ib__cost {
  position: absolute;
  inset-block-start: 0.2rem;
  inset-inline-end: 0.1rem;
  padding: 0.1rem 0.25rem;
  border-radius: 0.5rem;
  font-size: 0.6rem;
  font-weight: 700;
  line-height: 1;
  color: var(--ndx-ink-onink);
  background-color: var(--ndx-ink);
}

.ndx-ib[aria-disabled="true"] .ndx-ib__cost {
  background-color: var(--ndx-border);
}

.ndx-ib.js-copy.ndx-copied::after {
  position: absolute;
  inset-block-start: 100%;
  inset-inline-end: 0;
  z-index: 5;
  margin: 0;
  padding: 0.15rem 0.4rem;
  border-radius: var(--ndx-radius);
  color: var(--ndx-ink-onink);
  background-color: var(--ndx-ink);
  white-space: nowrap;
}

@media (max-width: 767.98px) {
  .ndx-ib {
    inline-size: 2.75rem;
    block-size: 2.75rem;
  }
}

/* The class + reason stacked under the domain name below md (W1): the cell
   is `.ndx-col-data` (mono) for the name, the stack reads in the body face. */
.ndx-relclass-stack {
  margin-block-start: 0.35rem;
  font-family: var(--ndx-sans);
  font-weight: 400;
}

.ndx-relreason {
  margin-block-start: 0.25rem;
  min-inline-size: 12rem;
}

/* Related page polish B (2026-10-03): below md the related table is a STACK
   -- each row a block, the Shared signals cell under the domain cell at full
   width -- so it never scrolls sideways at 375. Block display on the table's
   own boxes rather than a hidden second copy of the evidence, which would
   double every expand control in the DOM. `.d-none` columns stay hidden: its
   `!important` wins over this. The domain cell gives up its bottom rule so
   one row still reads as one row; the stripe sits on both cells. */
@media (max-width: 767.98px) {
  .ndx-reltable,
  .ndx-reltable > thead,
  .ndx-reltable > tbody,
  .ndx-reltable > thead > tr,
  .ndx-reltable > tbody > tr,
  .ndx-reltable > thead > tr > th,
  .ndx-reltable > tbody > tr > th,
  .ndx-reltable > tbody > tr > td {
    display: block;
  }

  .ndx-reltable > tbody > tr > th {
    border-block-end-width: 0;
    padding-block-end: 0.15rem;
  }

  .ndx-reltable > tbody > tr > td {
    padding-block-start: 0.15rem;
  }

  .ndx-reltable .ndx-relreason {
    min-inline-size: 0;
  }

  /* The Shared signals header: no column to head below md, but still the
     header of every evidence cell for a screen reader -- so visually hidden
     (Bootstrap's `.visually-hidden` recipe), never `display: none`. */
  .ndx-reltable > thead > tr > th.ndx-reltable__shared-head {
    position: absolute !important;
    inline-size: 1px !important;
    block-size: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
  }
}

/* --- saved investigations (roadmap D42 §6-§8, W2) ----------------------- */

/* Dismissed: the base .ndx-class colours ARE the dismissed tokens (grey,
   no strike-through since 2026-10-10: struck text hurt reading). */

.ndx-inv__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.5rem;
}

.ndx-inv__facts dt {
  font-weight: 500;
  color: var(--ndx-ink-muted);
}

.ndx-inv__facts dd {
  margin: 0;
}

.ndx-inv__desc {
  max-inline-size: var(--ndx-measure);
}

/* The action rows: forms sit inline beside links, wrapping on a phone. */
.ndx-inv__actions,
.ndx-inv__bulkrow,
.ndx-inv__filter,
.ndx-inv__pages,
.ndx-invsave {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.ndx-inv__filter .form-control {
  inline-size: auto;
  flex: 1 1 12rem;
  max-inline-size: 24rem;
}

.ndx-inv__select {
  inline-size: auto;
  max-inline-size: 14rem;
}

/* The type chips: smaller than the class tabs above them (they narrow a
   tab, they are not a second set of tabs), still a 24px target. Same
   prose-link exemption reasoning as `.ndx-tab-bar__link`. */
.ndx-inv__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}

.ndx-inv__chips .ndx-inv__chip {
  display: inline-flex;
  align-items: center;
  min-block-size: 2rem;
  padding-inline: 0.65rem;
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius-sm);
  background: var(--ndx-surface);
  font-size: 0.875rem;
  text-decoration: none;
  color: var(--ndx-ink-muted);
}

.ndx-inv__chips .ndx-inv__chip[aria-current="page"] {
  border-color: var(--ndx-ink);
  color: var(--ndx-ink);
  font-weight: 600;
}

.ndx-inv__marks {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-block-start: 0.35rem;
  font-family: var(--ndx-sans);
}

.ndx-inv__mark {
  font-weight: 400;
  color: var(--ndx-ink-muted);
  background-color: var(--ndx-sunken);
  border: 1px solid var(--ndx-border);
}

.ndx-inv__new {
  /* Neutral on purpose (owner-locked 2026-10-10): the brand accent never
     reads as a class colour, and "new" is not a class. */
  display: inline-block;
  padding: 0.05rem 0.45rem;
  border-radius: 999px;
  font-size: var(--ndx-fs-meta, 0.85rem);
  color: var(--ndx-ink);
  background-color: var(--ndx-sunken);
  border: 1px solid var(--ndx-border-strong);
  font-weight: 500;
}

.ndx-inv__gone {
  border-style: dashed;
}

.ndx-inv__item {
  min-inline-size: 14rem;
  font-weight: 400;
}

.ndx-inv__rowactions {
  min-inline-size: 16rem;
}

/* Row and page checkboxes at a 24px hit area (UX §8.3). */
.ndx-inv__table .form-check-input,
.ndx-inv__bulk .form-check-input {
  inline-size: 1.25rem;
  block-size: 1.25rem;
}

.ndx-inv__lower {
  display: grid;
  gap: 0 1rem;
}

@media (min-width: 768px) {
  .ndx-inv__lower {
    grid-template-columns: 1fr 1fr;
  }
}

/* 24px targets (UX §8.3) for the two kinds of control here that are text:
   the item's own link in the table, and the two disclosures' summaries. */
.ndx-inv__item a {
  display: inline-block;
  min-block-size: 1.5rem;
  line-height: 1.5rem;
}

.ndx-inv summary,
.ndx-inv__lower summary {
  min-block-size: 1.5rem;
  line-height: 1.5rem;
}

/* The table's visually-hidden labels (the select column header, each row's
   select label) are `position: absolute`; without a positioned scroller
   their containing block is the card, OUTSIDE the scroller's clip, and at
   375 they pushed the page sideways (e2e overflow sweep, W2). */
.ndx-inv__tabs .table-responsive {
  position: relative;
}

/* --- investigation completeness (2026-10-03) ----------------------------- */

/* The saved graph's node list carries its controls as POST forms (so they
   work with scripts off); inline, so a node's row reads like the Related
   page's links. */
.ndx-relnodes__form {
  display: inline;
}

.ndx-inv__nodes summary {
  min-block-size: 1.5rem;
  line-height: 1.5rem;
}

/* "Load more results": a control beside its sentence,
   wrapping on a phone. */
.ndx-inv__more,
.ndx-invlist__filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.ndx-invlist__filters .ndx-inv__chip[aria-current="page"] {
  border-color: var(--ndx-ink);
  color: var(--ndx-ink);
  font-weight: 600;
}

.ndx-invlist__filters .ndx-inv__chip {
  display: inline-flex;
  align-items: center;
  min-block-size: 2rem;
  padding-inline: 0.65rem;
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius-sm);
  background: var(--ndx-surface);
  font-size: 0.875rem;
  text-decoration: none;
  color: var(--ndx-ink-muted);
}

/* --- batch review fixes (2026-10-03) ------------------------------------- */

/* A saved investigation's tab table below md: each row a CARD -- the item,
   why, the shared values and the actions stacked at full width -- so no
   action sits behind a sideways scroll at 375. The Related table's recipe
   (polish B): block display on the table's own boxes, explicit ARIA roles in
   the template, the column headers visually hidden rather than removed. The
   select checkbox floats beside the item. Each stacked cell names itself with
   a small label (`.ndx-invtable__label`, aria-hidden: the column header is
   still the cell's header for a screen reader). */
.ndx-invtable__label {
  display: none;
}

@media (max-width: 767.98px) {
  .ndx-invtable,
  .ndx-invtable > thead,
  .ndx-invtable > tbody,
  .ndx-invtable > thead > tr,
  .ndx-invtable > tbody > tr,
  .ndx-invtable > tbody > tr > th,
  .ndx-invtable > tbody > tr > td {
    display: block;
  }

  .ndx-invtable > thead > tr > th {
    position: absolute !important;
    inline-size: 1px !important;
    block-size: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
  }

  .ndx-invtable > tbody > tr {
    margin-block-end: 0.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--ndx-border);
    border-radius: var(--ndx-radius);
    background: var(--ndx-surface);
  }

  .ndx-invtable > tbody > tr > th,
  .ndx-invtable > tbody > tr > td {
    padding: 0.25rem 0;
    border-block-end-width: 0;
    min-inline-size: 0;
    overflow-wrap: anywhere;
  }

  .ndx-invtable > tbody > tr > td.ndx-invtable__select {
    float: inline-end;
    padding: 0 0 0 0.5rem;
  }

  .ndx-invtable .ndx-invtable__label {
    display: block;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--ndx-ink-muted);
  }

  .ndx-invtable .ndx-inv__select {
    max-inline-size: 100%;
  }
}

/* The selection line under the explorer's toolbar: one line, never wrapping,
   so selecting a node (which changes its text) never moves the picture
   below it between the two clicks of a double-click (polish B ruling 9a). */
.ndx-relex__selection {
  flex: 1 1 100%;
  min-inline-size: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--ndx-ink-muted);
}

/* 24px targets (UX §8.3, WCAG 2.5.8; batch review 2026-10-03) for three text
   controls the tap-target sweep skips because they sit in prose-like boxes:
   the facet rail's summary, the lookalike row's "List them" summary and the
   domain link in a related table's row header. */
.ndx-ws__rail > summary,
.ndx-rellookalikes__details > summary,
.ndx-reltable > tbody > tr > th > a {
  display: inline-block;
  min-block-size: 1.5rem;
  line-height: 1.5rem;
}

.ndx-ws__rail > summary,
.ndx-rellookalikes__details > summary {
  display: list-item;
}

/* Owner-approved Tools enrichment: specialized Lookup evidence uses D43's
   table and menu shell, including phone cards rather than horizontal tables. */
.ndx-lookup-form .ndx-lookup-input,
.ndx-lookup-form button[type="submit"],
.ndx-lookup-form .ndx-rtype-select { block-size: 3.375rem; min-block-size: 3.375rem; }
.ndx-lookup-form button[type="submit"] { align-self: stretch; }
.ndx-lookup-form .ndx-rtype-select { max-inline-size: 30rem; }
.ndx-lookup-table { table-layout: auto; }
.ndx-lookup-table .ndx-dt__actions { min-inline-size: 3.5rem; }
.ndx-lookup-results { overflow: visible; }
@media (max-width: 47.999rem) {
  .ndx-lookup-table .ndx-dt__row { grid-template-columns: minmax(0, 1fr) auto; }
  .ndx-lookup-table .ndx-dt__row > th,
  .ndx-lookup-table .ndx-dt__row > td { grid-column: 1 / -1; }
  .ndx-lookup-table .ndx-dt__row > th { grid-column: 1; grid-row: 1; }
  .ndx-lookup-table .ndx-dt__row > .ndx-dt__actions { grid-column: 2; grid-row: 1; }
  .ndx-lookup-table td[data-label]::before { content: attr(data-label) ": "; font-family: var(--ndx-sans); font-weight: 500; }
  .ndx-lookup-table .ndx-col-name { white-space: normal; overflow-wrap: anywhere; }
  .ndx-lookup-table .ndx-col-value { max-inline-size: none; }
}

/* Tools enrichment gate: small help text made the native summaries 22px
   tall. Keep their disclosure markers and meet the existing 24px tap floor
   while scaling the target with the reader's root text size. */
.ndx-lookup-help > summary {
  min-block-size: max(1.5rem, 24px);
  min-inline-size: max(1.5rem, 24px);
  line-height: max(1.5rem, 24px);
}

/* Discovery showcase: readable full-width captures, shared by home and guides.
   Native image geometry is intentional: never crop evidence or manufacture a
   new interface from the owner-supplied screenshots. */
.ndx-product-figure { margin: 0; min-inline-size: 0; }
.ndx-product-figure__image { display: block; }
.ndx-product-figure img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius);
  background: var(--ndx-surface);
}
.ndx-product-figure__fullsize {
  display: inline-block;
  margin-block: .5rem;
  min-block-size: max(1.5rem, 24px);
  font-size: var(--ndx-fs-small);
}
.ndx-product-figure figcaption { margin-block-end: 1.5rem; font-size: var(--ndx-fs-small); }
.ndx-product-figure figcaption p { margin-block-end: .25rem; color: var(--ndx-ink-muted); }
.ndx-product-figure figcaption > a { display: inline-block; min-block-size: max(1.5rem, 24px); }
.ndx-docs__body .ndx-product-figure { margin-block: 1.5rem; }

/* Homepage-only editorial composition (2026-10-06 owner design review).
   Each frame uses exact source coordinates in CSS. Original screenshot files
   and the uncropped documentation figures remain unchanged. No motion. */
.ndx-hero__layout--editorial { gap: 2rem; align-items: center; }
.ndx-hero__layout--editorial .ndx-hero__copy { position: relative; z-index: 1; }
.ndx-discovery-art,
.ndx-discovery-secondary__art { min-inline-size: 0; margin: 0; }
.ndx-discovery-art__stage { position: relative; padding-block-end: 5rem; }
.ndx-discovery-art__graph,
.ndx-discovery-art__evidence,
.ndx-discovery-secondary__results { position: relative; overflow: hidden; }
.ndx-discovery-art__graph {
  aspect-ratio: 910 / 450;
  mix-blend-mode: multiply;
  /* Fade the screenshot itself, not a tinted rectangular overlay. The
     center graph stays opaque while captured panel rules dissolve at edges. */
  mask-image: linear-gradient(to right, transparent, black 13%, black 87%, transparent),
    linear-gradient(to bottom, transparent, black 14%, black 78%, transparent);
  mask-composite: intersect;
}
.ndx-discovery-art__graph img,
.ndx-discovery-art__evidence img,
.ndx-discovery-secondary__results img { position: absolute; max-inline-size: none; block-size: auto; }
.ndx-discovery-art__graph img { inline-size: 161.98%; inset-inline-start: -26.37%; inset-block-start: -37.78%; }
.ndx-discovery-art__evidence {
  position: absolute; inline-size: 80%; aspect-ratio: 638 / 259;
  inset-inline-start: 4%; inset-block-end: 1rem;
  border: .25rem solid var(--ndx-surface); border-radius: var(--ndx-radius);
  box-shadow: var(--ndx-shadow-pop); background: var(--ndx-surface);
}
.ndx-discovery-art__evidence img { inline-size: 231.03%; inset-inline-start: -8.15%; inset-block-start: -231.27%; }
.ndx-discovery-art figcaption,
.ndx-discovery-secondary__art figcaption {
  display: flex; flex-wrap: wrap; gap: .25rem 1rem;
  margin-block-start: .5rem; font-size: var(--ndx-fs-small); color: var(--ndx-ink-muted);
}
.ndx-discovery-art figcaption a,
.ndx-discovery-secondary a { display: inline-block; min-block-size: max(1.5rem, 24px); }
.ndx-discovery-secondary { display: grid; gap: 2rem; align-items: center; }
.ndx-discovery-secondary__copy { min-inline-size: 0; }
.ndx-discovery-secondary__results {
  aspect-ratio: 1296 / 600; border: 1px solid var(--ndx-border);
  border-radius: var(--ndx-radius); box-shadow: var(--ndx-shadow-pop);
  background: var(--ndx-surface);
}
.ndx-discovery-secondary__results img { inline-size: 111.19%; inset-inline-start: -6.48%; inset-block-start: -68%; }
.ndx-discovery-secondary__results::after {
  position: absolute; inset: 0; content: ""; pointer-events: none;
  background: linear-gradient(to top, var(--ndx-paper), transparent 30%);
}
@media (min-width: 62rem) {
  .ndx-hero__layout--editorial { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: 1.5rem; }
  .ndx-discovery-art { inline-size: calc(100% + 2rem); }
  .ndx-hero__layout--editorial { overflow: clip; }
  .ndx-discovery-art__stage { padding-block-end: 6rem; }
  .ndx-discovery-secondary { grid-template-columns: minmax(0, 7fr) minmax(0, 13fr); gap: 3rem; }
}

@media (max-width: 47.999rem) {
  .ndx-discovery-art__evidence { display: none; }
  .ndx-discovery-art__stage { padding-block-end: 0; }
}

/* Shared editorial rhythm, extended from the owner-approved discovery hero.
   These are explicit page hooks, not global card/table overrides: working
   Investigations retain their locked flat layout and data-table geometry. */
:root {
  --ndx-space-page: clamp(1.5rem, 3vw, 3rem);
  --ndx-space-section: clamp(1.5rem, 2.5vw, 2.5rem);
  --ndx-space-panel: clamp(1rem, 2vw, 1.5rem);
}

.ndx-page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: space-between;
  gap: 1rem 2rem;
  padding-block: var(--ndx-space-page) var(--ndx-space-section);
  margin-block-end: var(--ndx-space-section);
  border-block-end: thin solid var(--ndx-border);
}

.ndx-page-header > h1 { margin-block: 0; }
.ndx-page-header > p { flex-basis: 100%; margin-block: 0; }
.ndx-page-header > div { min-inline-size: 0; }
.ndx-page-header h1 { margin-block-end: 0.5rem; }
.ndx-page-header p:last-child { margin-block-end: 0; }
.ndx-page-header--compact { padding-block: 0.5rem 1rem; margin-block-end: 1.25rem; }
.ndx-page-header--compact h1 { margin-block-end: 0; }
.ndx-page-intro { color: var(--ndx-ink-muted); max-inline-size: var(--ndx-measure); }
.ndx-page-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; }
.ndx-page-actions .btn { display: inline-flex; align-items: center; justify-content: center; min-block-size: 2.5rem; }
.ndx-reading-page { max-inline-size: 76ch; margin-inline: auto; }
.ndx-reading-page h2 { margin-block-start: var(--ndx-space-section); }
.ndx-reading-page li + li { margin-block-start: 0.5rem; }
.ndx-account-page { max-inline-size: 64rem; margin-inline: auto; }
.ndx-account-page > .card { margin-block-end: var(--ndx-space-section); }
.ndx-account-page > .card > .card-body { padding: var(--ndx-space-panel); }
.ndx-docs__body { min-inline-size: 0; }
.ndx-docs__body h2[id] { margin-block-start: var(--ndx-space-section); padding-block-start: 1rem; border-block-start: thin solid var(--ndx-border); }
.ndx-docs__rail { padding-block: 1rem; }
.ndx-docs__rail a { display: inline-block; min-block-size: 1.5rem; padding-block: 0.2rem; }
.ndx-docs__body .ndx-product-figure { margin-block: var(--ndx-space-section); }
.ndx-tool-page .ndx-page-header h1 { font-size: 1.5rem; }
.ndx-tool-page main > .card > .card-body { padding: var(--ndx-space-panel); }
.ndx-entrance > main > .card { border-block-start: 0.2rem solid var(--ndx-accent); }
.ndx-entrance > main > .card > .card-body,
.ndx-entrance > main > .card.card-body { padding: var(--ndx-space-panel); }

@media (min-width: 62rem) {
  .ndx-docs__rail { border-inline-start: thin solid var(--ndx-border); padding-inline-start: 2rem; }
}

@media (max-width: 47.999rem) {
  .ndx-page-header { padding-block-start: 1rem; }
  .ndx-page-actions { inline-size: 100%; }
  .ndx-reading-page { max-inline-size: 100%; }
}

.ndx-page-header__back { flex-basis: 100%; font-size: var(--ndx-fs-small); min-block-size: 1.5rem; }
.ndx-docs-directory,
.ndx-docs-resources { margin-block-start: var(--ndx-space-section); }
.ndx-guide-choice { block-size: 100%; padding-block: 1.25rem; border-block-start: thin solid var(--ndx-border); }
.ndx-guide-choice__body { display: flex; flex-direction: column; align-items: start; block-size: 100%; gap: 0.5rem; }
.ndx-guide-choice__body > :is(h2, p) { margin-block: 0; }
.ndx-guide-choice__link { display: inline-flex; align-items: center; min-block-size: 2rem; margin-block-start: auto; }

/* Prose headers read vertically; action headers may share a row. */
.ndx-page-header:has(> .ndx-page-intro) > h1 { flex-basis: 100%; }
.ndx-docs__body > h2[id]:first-child { margin-block-start: 0; padding-block-start: 0; border-block-start: 0; }

.ndx-guide-choice { display: flex; flex-direction: column; gap: 1.25rem; }
.ndx-guide-choice__body { block-size: auto; flex: 1; }

/* Keep visually hidden captions inside their scroll region's coordinate
   system; otherwise a short account table can gain a spurious scrollbar. */
.ndx-account-page .table-responsive { position: relative; }

/* Account settings: keep security forms readable and QR setup usable at 375px. */
.ndx-settings { max-inline-size: 48rem; }
.ndx-settings-section { padding-block: 1.5rem; border-block-end: 1px solid var(--bs-border-color); }
.ndx-settings nav a { display: inline-flex; align-items: center; min-block-size: 2.75rem; }
.ndx-settings img { max-inline-size: 100%; block-size: auto; }
.ndx-settings input, .ndx-settings textarea { max-inline-size: 100%; }

/* The dashboard history stays compact even with long queries. The labelled,
   focusable region supports keyboard scrolling without hiding older links. */
.ndx-history-scroll {
  max-height: 16rem;
  overflow-y: auto;
}

.ndx-history-scroll thead th {
  position: sticky;
  top: 0;
  background: var(--bs-body-bg);
  z-index: 1;
}

/* Icons outside the investigation lists (2026-10-10): the destructive icon
   button (API key revoke), the navbar toggler glyph, the leading lookup icon
   on the dashboard's recent lookups, and the aria-disabled look of a .btn
   (Bootstrap only styles the `disabled` attribute and `.disabled`). */
.ndx-ib--danger:hover,
.ndx-ib--danger:focus-visible {
  color: var(--ndx-danger);
}

.navbar-toggler .bi {
  font-size: 1.75rem;
  line-height: 1;
}

.ndx-history-icon {
  display: inline-block;
  inline-size: 1.25em;
  color: var(--ndx-ink-muted);
}

.btn[aria-disabled="true"] {
  opacity: 0.65;
  cursor: not-allowed;
}
