/* ==========================================================================
   FRONTGRADE  ·  SHARED COMPONENTS  ·  v2
   Requires tokens.css to be loaded first.
   Vanilla CSS. No frameworks, no build step, no external dependencies.

   Contrast notes in this file quote ratios computed from the sRGB relative
   luminance formula against the exact surface the element sits on, with any
   alpha composited first. Where a combination fails, the comment forbids the
   combination outright; the single orange #e35530 is never recoloured.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. BASE
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  background: var(--surface-base);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--surface-base);
  color: var(--text-muted);
  font: var(--type-body);
  letter-spacing: var(--ls-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img, svg, video { display: block; max-width: 100%; }
svg { fill: currentColor; }

::selection {
  background: var(--accent);
  color: var(--text-on-accent);   /* 5.22:1 */
}

/* Thin scrollbar. Decorative, and it inherits the surface ramp so it never
   reads as a second border on a dark panel. */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.10); }
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.18); }

hr {
  border: 0;
  border-top: var(--border-hairline-w) solid var(--border-hairline);
  margin: var(--space-9) 0;
}

/* --------------------------------------------------------------------------
   2. TYPOGRAPHY PRIMITIVES
   Display faces are Barlow Condensed bold, uppercase, set to leading-none.
   Everything else is Inter, small and quiet.
   -------------------------------------------------------------------------- */

h1, h2, h3, h4, p, ul, ol, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }

.fg-display,
.fg-h1,
.fg-h2,
.fg-h3 {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  color: var(--text-display);        /* 17.90:1 base, 15.39:1 elevated */
  text-transform: uppercase;
  text-wrap: balance;
}

.fg-display { font-size: var(--fs-display); line-height: var(--lh-display); letter-spacing: var(--ls-display); }
.fg-h1      { font-size: var(--fs-h1);      line-height: var(--lh-display); letter-spacing: var(--ls-h1); }
.fg-h2      { font-size: var(--fs-h2);      line-height: var(--lh-heading); letter-spacing: var(--ls-h2); }
.fg-h3      { font-size: var(--fs-h3);      line-height: var(--lh-h3);      letter-spacing: var(--ls-h3); }

/* Inter sub-head. Sentence case, used for tile and row labels. */
.fg-h4 {
  font-family: var(--font-body);
  font-size: var(--fs-h4);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  color: var(--text-primary);
  text-transform: none;
  letter-spacing: 0;
}

/* One highlighted word inside a display line.
   --accent measures 5.22 / 4.98 / 4.68 / 4.49 across the ladder. At display
   sizes the applicable threshold is the 3:1 large-text rule, which all four
   clear with room to spare. */
.fg-accent-word { color: var(--accent); }

.fg-lede {
  font-size: var(--fs-lede);
  line-height: var(--lh-lede);
  color: var(--text-muted);          /* 5.57:1 base, 4.79:1 elevated */
  max-width: var(--measure-lede);
  text-wrap: pretty;
}

.fg-body  { font-size: var(--fs-body);  line-height: var(--lh-body); color: var(--text-muted); max-width: var(--measure-body); }
.fg-small { font-size: var(--fs-small); line-height: 1.55;           color: var(--text-muted); }

/* Long-form reading copy. The blog article body is the only place this is
   used; every other surface follows the low-copy budget. */
.fg-prose { font-size: var(--fs-prose); line-height: var(--lh-prose); color: var(--text-muted); max-width: var(--measure-prose); }

/* Meta line: date, read time, category rail. */
.fg-meta {
  font-size: var(--fs-2xs);
  line-height: 1.4;
  color: var(--text-muted);
  letter-spacing: var(--ls-chip);
}

/* Eyebrow. 12px / 600 / 0.22em, orange.
   PERMITTED on --surface-base (5.22), --surface-raised (4.98) and
   --surface-panel (4.68). FORBIDDEN on --surface-elevated (4.49). */
.fg-eyebrow {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--space-3);
}
.fg-eyebrow--muted { color: var(--text-muted); }

/* 11px column / group title. Muted, never orange. */
.fg-label {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Stat numeral. Barlow Condensed bold, orange, large-text rule applies. */
.fg-stat {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: var(--fs-stat);
  line-height: 1;
  color: var(--accent);
}

/* Section opener: eyebrow + heading + optional lede. */
.fg-section-head { margin-bottom: clamp(28px, 1.6rem + 1.6vw, 48px); }
.fg-section-head .fg-lede { margin-top: var(--space-4); }
.fg-section-head--center { text-align: center; }
.fg-section-head--center .fg-lede { margin-inline: auto; }
.fg-section-head--tight  { margin-bottom: var(--space-7); }

/* Split opener: copy left, a single quiet link right. */
.fg-section-head--split {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
  flex-wrap: wrap;
}

/* --------------------------------------------------------------------------
   3. LAYOUT  ·  containers, bands, rules, grids
   -------------------------------------------------------------------------- */

.fg-container {
  width: 100%;
  max-width: calc(var(--container-max) + (var(--gutter) * 2));
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.fg-container--wide { max-width: calc(var(--container-wide) + (var(--gutter) * 2)); }
.fg-container--text { max-width: calc(var(--container-text) + (var(--gutter) * 2)); }

/* Section bands. v2 separates sections with hairline rules and small tone
   shifts, not heavy alternating gradients. */
.fg-band {
  position: relative;
  padding-block: var(--section-y);
  background: var(--surface-base);
}
.fg-band--base     { background: var(--surface-base); }
.fg-band--raised   { background: var(--surface-raised); }
.fg-band--panel    { background: var(--surface-panel); }
.fg-band--tall     { padding-block: var(--section-y-lg); }
.fg-band--short    { padding-block: var(--section-y-sm); }
.fg-band--flush-top    { padding-top: 0; }
.fg-band--flush-bottom { padding-bottom: 0; }
.fg-band--hairline { border-top: var(--border-hairline-w) solid var(--border-hairline); }

/* Alternation utility. Explicit .fg-band--* classes always win, so a page can
   opt one band out without breaking the sequence. */
.fg-bands > .fg-band { border-top: var(--border-hairline-w) solid var(--border-hairline); }
.fg-bands > .fg-band:first-child { border-top: 0; }
.fg-bands > .fg-band:nth-of-type(even) { background: var(--surface-raised); }
.fg-bands--invert > .fg-band:nth-of-type(odd)  { background: var(--surface-raised); }
.fg-bands--invert > .fg-band:nth-of-type(even) { background: var(--surface-base); }

/* Standalone hairline rule. */
.fg-rule {
  height: var(--border-hairline-w);
  border: 0;
  margin: 0;
  background: var(--border-hairline);
}
.fg-rule--short { width: 32px; height: 1px; background: var(--accent); }

.fg-grid { display: grid; gap: var(--grid-gap); }
.fg-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.fg-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.fg-grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.fg-grid--6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }

@media (max-width: 1280px) { .fg-grid--6 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 1024px) { .fg-grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 768px)  { .fg-grid--3, .fg-grid--6 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .fg-grid--2, .fg-grid--3, .fg-grid--4, .fg-grid--6 { grid-template-columns: minmax(0, 1fr); } }

/* Hairline-seam grid: cells butt together and the 1px gap reads as the rule. */
.fg-grid--seam { gap: var(--border-hairline-w); background: var(--border-hairline); }
.fg-grid--seam > * { background: var(--surface-raised); }

.fg-stack { display: flex; flex-direction: column; gap: var(--stack-gap); }
.fg-row   { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }

/* --------------------------------------------------------------------------
   4. FOCUS  ·  two-tone ring on every interactive element
   2px white outline at a 2px offset, the offset gap filled by a dark shadow,
   and a 1px accent edge outside it. Precise, not chunky. `outline` is kept
   alongside box-shadow so forced-colors mode retains an indicator.
   -------------------------------------------------------------------------- */

:where(a, button, input, select, textarea, summary, [tabindex]):focus { outline: none; }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible,
.fg-focus-ring:focus-visible {
  outline: var(--focus-ring-width) solid var(--focus-ring-inner);
  outline-offset: var(--focus-ring-offset);
  box-shadow:
    0 0 0 var(--focus-ring-offset) var(--focus-ring-outer),
    0 0 0 5px var(--focus-ring-halo);
  border-radius: 0;
}

@media (forced-colors: active) {
  :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 2px;
  }
}

.fg-skip-link {
  position: fixed;
  top: var(--space-3);
  left: var(--space-3);
  z-index: var(--z-skip);
  transform: translateY(calc(-100% - var(--space-9)));
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: var(--space-3) var(--space-5);
  background: var(--accent);
  color: var(--text-on-accent);        /* 5.22:1 */
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  text-decoration: none;
  transition: transform var(--dur-quick) var(--ease-out-expo);
}
.fg-skip-link:focus-visible { transform: translateY(0); }

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

/* --------------------------------------------------------------------------
   5. BUTTONS AND LINKS  ·  sharp corners, compact
   -------------------------------------------------------------------------- */

.fg-btn {
  --btn-py: 12px;
  --btn-px: 26px;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--btn-py) var(--btn-px);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: var(--ls-btn);
  text-decoration: none;
  white-space: nowrap;
  border: var(--border-hairline-w) solid transparent;
  border-radius: 0;
  cursor: pointer;
  transition: var(--t-color), transform var(--dur-fast) var(--ease-out-quart);
  -webkit-appearance: none;
  appearance: none;
}

/* Primary. Near-black ink on the one orange.
     --text-on-accent on --accent  5.22:1  AA
   White on this fill computes 3.74:1 and is never used.
   HOVER RULE: the fill does NOT darken. Darkening it under a near-black
   label would drive the label contrast down, so hover is carried by a light
   outline and a 1px lift, and the measured 5.22:1 holds in every state. */
.fg-btn--primary {
  background: var(--accent);
  color: var(--text-on-accent);
  border-color: var(--accent);
}
.fg-btn--primary:hover {
  background: var(--accent);
  border-color: rgba(255, 255, 255, 0.72);
  color: var(--text-on-accent);
  transform: translateY(-1px);
}
.fg-btn--primary:active { transform: translateY(0); border-color: rgba(255, 255, 255, 0.9); }

/* Secondary. Transparent with a functional 1px boundary.
     --border-ui carries the control edge at 3.36:1 worst case
     --text-primary label at 13.95:1 worst case */
.fg-btn--secondary {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--border-ui);
}
.fg-btn--secondary:hover {
  border-color: var(--border-strong);   /* 4.79:1 worst case */
  background: rgba(255, 255, 255, 0.035);
  transform: translateY(-1px);
}
.fg-btn--secondary:active { background: rgba(255, 255, 255, 0.07); transform: translateY(0); }

/* Ghost. Text plus arrow, no chrome.
   Orange label: permitted on base, raised and panel; not on elevated. */
.fg-btn--ghost {
  padding-inline: 0;
  background: none;
  border-color: transparent;
  color: var(--accent);
}
.fg-btn--ghost:hover { color: var(--text-display); }

.fg-btn--sm { --btn-py: 10px; --btn-px: 20px; font-size: var(--fs-xs); }

/* OUT OF CONTRACT, AND DOWN TO ONE CALLER. This box draws 46px against the
   contract's --fg-btn-h of 40px, and the audit found it on the closing band
   of every redesigned family, so a primary and a secondary sitting side by
   side in a .fg-btn-group were 46, 40 and 32px against each other depending
   on which band they were in. The token has been taken off every closing
   band in the theme; the front page's hero pair is the one place still
   asking for it, and that file is under separate work. The declaration stays
   for that caller rather than being aliased to the standard box, because
   aliasing would move a page this pass does not own without its owner seeing
   the change. Removing ` fg-btn--lg` from those two lines is what retires
   the rule. */
.fg-btn--lg { --btn-py: 14px; --btn-px: 30px; font-size: var(--fs-body); }
.fg-btn--block { width: 100%; }

.fg-btn__arrow { width: 13px; height: 13px; flex: none; transition: transform var(--dur-quick) var(--ease-out-expo); }
.fg-btn:hover .fg-btn__arrow { transform: translateX(3px); }

.fg-btn-group { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.fg-btn-group--stack { max-width: 100%; }

@media (max-width: 560px) {
  /* width:100% matters: inside a flex parent the group would otherwise size
     to the longest label's max-content width and push past the container. */
  .fg-btn-group--stack { flex-direction: column; align-items: stretch; width: 100%; }
  .fg-btn-group--stack .fg-btn { width: 100%; white-space: normal; line-height: 1.3; text-align: center; }
}

/* Quiet link with an arrow that lights up. */
.fg-link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding-block: 4px;
  font-family: var(--font-body);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-btn);
  color: var(--text-muted);            /* 5.57:1 base */
  text-decoration: none;
  transition: var(--t-color);
}
.fg-link-arrow svg { width: 13px; height: 13px; transition: transform var(--dur-quick) var(--ease-out-expo); }
.fg-link-arrow:hover { color: var(--accent); }
.fg-link-arrow:hover svg { transform: translateX(3px); }

/* Inline body link. */
.fg-link {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: var(--t-color);
}
.fg-link:hover { color: var(--text-display); text-decoration-color: var(--accent); }

/* --------------------------------------------------------------------------
   6. CARD  ·  hairline panel, sharp corners, orange hover edge
   -------------------------------------------------------------------------- */

.fg-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface-raised);
  border: var(--border-hairline-w) solid var(--border-hairline);
  border-radius: 0;
  overflow: hidden;
  transition: var(--t-color), transform var(--dur-base) var(--ease-out-expo);
}
.fg-card--flush { background: transparent; }
.fg-card--panel { background: var(--surface-panel); }

.fg-card__media { position: relative; }
.fg-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: clamp(18px, 1rem + 0.8vw, 24px);
  flex: 1;
}
.fg-card__title {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: var(--fs-h3);
  line-height: var(--lh-h3);
  letter-spacing: var(--ls-h3);
  text-transform: uppercase;
  color: var(--text-display);
  transition: var(--t-color);
}
/* Inter variant, for compact news and list cards. */
.fg-card__title--sm {
  font-family: var(--font-body);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-small);
  line-height: 1.4;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-primary);
}
.fg-card__text   { font-size: var(--fs-xs); line-height: 1.6; color: var(--text-muted); }
.fg-card__footer { margin-top: auto; padding-top: var(--space-4); }

/* Short accent rule under the card opener. It marks the head of a card
   without adding a full divider across it. */
.fg-card__rule { width: 20px; height: 1px; background: var(--accent); border: 0; margin: 0 0 var(--space-1); }

/* Whole-card link. Hover raises the hairline to the brand edge and lays a
   5% accent tint over the surface.
     card surface --surface-raised, tinted #191521
       --text-display 14.87:1 · --text-muted 5.10:1 · --accent 4.79:1
   The orange title on hover therefore clears AA. Cards must stay on
   --surface-base or --surface-raised: on --surface-panel the same tint puts
   an orange label at 4.48:1, which fails. */
.fg-card--link { cursor: pointer; }
.fg-card--link:hover,
.fg-card--link:focus-within {
  border-color: var(--border-accent);   /* 4.98:1 on raised, clears 3:1 */
  background-color: var(--surface-raised);
  background-image: linear-gradient(var(--accent-tint), var(--accent-tint));
  transform: translateY(-2px);
}
.fg-card--link:hover .fg-card__title,
.fg-card--link:focus-within .fg-card__title { color: var(--accent); }
.fg-card--link .fg-card__title a { color: inherit; text-decoration: none; }
/* Stretched hit area. The visible ring is drawn on the card, not the text. */
.fg-card--link .fg-card__title a::after { content: ""; position: absolute; inset: 0; }
.fg-card--link:has(a:focus-visible) {
  outline: var(--focus-ring-width) solid var(--focus-ring-inner);
  outline-offset: var(--focus-ring-offset);
  box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-ring-outer),
              0 0 0 5px var(--focus-ring-halo);
}
.fg-card--link .fg-card__title a:focus-visible { outline: none; box-shadow: none; }

/* --------------------------------------------------------------------------
   7. CHIP, TAG BADGE
   -------------------------------------------------------------------------- */

/* Bordered, never filled. Hairline at white/0.14, 12px semibold label. */
.fg-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 6px 12px;
  background: transparent;
  border: var(--border-hairline-w) solid var(--border-hairline-strong);
  border-radius: 0;
  font-family: var(--font-body);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  line-height: 1.2;
  letter-spacing: var(--ls-chip);
  color: var(--text-primary);          /* 16.22:1 base, 13.95:1 elevated */
  text-decoration: none;
  /* Keeps a chip its own width inside a column flex parent such as
     .fg-card__body, and top-aligns it inside a wrapped .fg-chip-row. */
  align-self: flex-start;
  transition: var(--t-color);
}
a.fg-chip:hover,
button.fg-chip:hover {
  border-color: var(--border-accent);  /* clears 3:1 on every surface */
  color: var(--accent);                /* permitted on base / raised / panel */
}
.fg-chip--accent { border-color: var(--border-accent); color: var(--accent); }
.fg-chip--ghost  { border-color: var(--border-hairline); color: var(--text-muted); }

/* Selected filter state. Carries a glyph as well as colour. */
.fg-chip[aria-pressed="true"],
.fg-chip--active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-on-accent);        /* 5.22:1 */
}

.fg-chip-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* Tag badge: 11px bold, 0.15em, uppercase. Filled with near-black ink.
     --text-on-accent on --accent  5.22:1  AA
   White on the fill computes 3.74:1 and is not used at this size. */
.fg-tag {
  display: inline-flex;
  align-items: center;
  padding: 5px 10px;
  background: var(--accent);
  color: var(--text-on-accent);
  font-family: var(--font-body);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-bold);
  line-height: 1.2;
  letter-spacing: var(--ls-badge);
  text-transform: uppercase;
  text-decoration: none;
  border: var(--border-hairline-w) solid var(--accent);
  align-self: flex-start;
}
/* Quiet variant: hairline plus muted label. */
.fg-tag--muted {
  background: transparent;
  border-color: var(--border-hairline-strong);
  color: var(--text-muted);            /* 5.57:1 base, 4.99:1 panel */
}
/* Outline variant: orange label on the surface.
   PERMITTED on base (5.22), raised (4.98) and panel (4.68).
   FORBIDDEN on --surface-elevated (4.49). */
.fg-tag--outline {
  background: transparent;
  border-color: var(--border-accent);
  color: var(--accent);
}

/* --------------------------------------------------------------------------
   8. ICON IN A BORDERED SQUARE  ·  the v2 icon pattern
   Thin-line glyph, 1.5-2px stroke, inside a 40px hairline square.
   -------------------------------------------------------------------------- */

.fg-icon-square {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px; height: 40px;
  flex: none;
  background: var(--surface-base);
  border: var(--border-hairline-w) solid var(--border-hairline-strong);
  color: var(--accent);                /* icon stroke, 3:1 minimum, 4.49 worst */
  transition: var(--t-color);
}
.fg-icon-square svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; }
.fg-icon-square--sm { width: 28px; height: 28px; }
.fg-icon-square--sm svg { width: 14px; height: 14px; }
.fg-icon-square--accent { border-color: var(--border-accent); }

/* Thin-line icon on its own. */
.fg-icon-line { width: 20px; height: 20px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.6; color: var(--accent); }

/* --------------------------------------------------------------------------
   9. IMAGE PLACEHOLDER  ·  CSS only, no assets
   Dark navy well, faint hex lattice, a single orange trace, and a label chip
   that is always the topmost layer of the component.
   -------------------------------------------------------------------------- */

.fg-placeholder {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  border-radius: 0;
  background:
    radial-gradient(120% 90% at 78% 12%, rgba(227, 85, 48, 0.10) 0%, rgba(227, 85, 48, 0) 58%),
    linear-gradient(152deg, #1b2233 0%, #12172a 46%, #0e1220 100%);
  border: var(--border-hairline-w) solid var(--border-hairline);
}

/* Hex lattice. Decorative; the component's own aria-label carries meaning. */
.fg-placeholder::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34.64' height='60' viewBox='0 0 34.64 60'%3E%3Cpath d='M17.32 0 L0 10 L0 30 L17.32 40 L34.64 30 L34.64 10 Z M17.32 40 L17.32 60' fill='none' stroke='%237d89a3' stroke-width='1'/%3E%3C/svg%3E");
  background-size: 34.64px 60px;
  opacity: 0.14;
  mask-image: radial-gradient(115% 110% at 50% 42%, #000 10%, rgba(0, 0, 0, 0.3) 72%, transparent 100%);
  -webkit-mask-image: radial-gradient(115% 110% at 50% 42%, #000 10%, rgba(0, 0, 0, 0.3) 72%, transparent 100%);
  pointer-events: none;
}

/* Two signal traces. */
.fg-placeholder::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(90deg, transparent 0 7%,  rgba(227, 85, 48, 0.30) 7% 7.14%,  transparent 7.14% 100%),
    linear-gradient(0deg,  transparent 0 21%, rgba(125, 137, 163, 0.16) 21% 21.4%, transparent 21.4% 100%);
  mask-image: linear-gradient(115deg, #000 0%, rgba(0, 0, 0, 0.15) 62%, transparent 92%);
  -webkit-mask-image: linear-gradient(115deg, #000 0%, rgba(0, 0, 0, 0.15) 62%, transparent 92%);
  pointer-events: none;
}

/* Label chip. z-index 3 puts it above every layer of the component AND above
   a media-band scrim, which sits at z-index 1 in the band (fix: the label is
   never dimmed by a scrim).
   The chip paints its own 0.78 ink backing, so its contrast does not depend
   on what is behind it. Measured against the worst case, the chip laid over
   the lightest point the well can reach (#2f2733):
     chip backing #11121b   --text-muted 5.31:1 · --accent 4.98:1 · --text-display 17.07:1
   And over the thinnest media-band scrim stop (0.45) at that same point:
     chip backing #0e0f18   --text-muted 5.43:1 · --accent 5.09:1 · --text-display 17.47:1 */
.fg-placeholder__label {
  position: relative;
  z-index: 3;
  max-width: 74%;
  padding: 10px 16px;
  text-align: center;
  font-family: var(--font-body);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  line-height: 1.45;
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-muted);
  background: rgba(9, 12, 20, 0.78);
  border: var(--border-hairline-w) solid var(--border-hairline-strong);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.fg-placeholder__label b {
  display: block;
  color: var(--accent);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-badge);
  margin-bottom: 3px;
}

.fg-placeholder--hero     { aspect-ratio: 21 / 9; border: 0; }
.fg-placeholder--wide     { aspect-ratio: 21 / 9; }
.fg-placeholder--square   { aspect-ratio: 1 / 1; }
.fg-placeholder--portrait { aspect-ratio: 4 / 5; }
.fg-placeholder--card     { aspect-ratio: 16 / 10; border: 0; border-bottom: var(--border-hairline-w) solid var(--border-hairline); }
/* z-index:auto is deliberate: it keeps the fill from opening a stacking
   context, so the label above can rise past a sibling scrim. */
.fg-placeholder--fill     { aspect-ratio: auto; height: 100%; position: absolute; inset: 0; z-index: auto; }

/* --------------------------------------------------------------------------
   10. HEADER  ·  utility bar + compact 56px row + dropdown cards
   -------------------------------------------------------------------------- */

/* padding-top lifts the row off the viewport edge; ::before and ::after
   resolve against the padding box, so the fill and scrim still reach y=0 and
   only the content moves. --header-offset in tokens.css counts it. */
.fg-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: var(--z-header);
  padding-top: 6px;
  background: transparent;
}
/* Hero scrim. Taller than the header and fading BELOW it, so the alpha stays
   at or above 0.80 everywhere the header type sits while the visible effect
   is a soft vignette over the hero rather than a bar. Contrast math is on
   --header-scrim in tokens.css. */
.fg-header::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 0;
  height: var(--header-scrim-h);
  z-index: -2;
  pointer-events: none;
  background: var(--header-scrim);
  transition: opacity var(--dur-base) var(--ease-standard);
}
/* Solid fill fades in on scroll and whenever a menu is open. Painted as a
   pseudo-element so the transition is a real cross-fade, not a snap. */
.fg-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--header-solid);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.fg-header.is-scrolled::before,
.fg-header[data-scrolled="true"]::before,
.fg-header[data-menu-open="true"]::before { opacity: 1; }
.fg-header.is-scrolled::after,
.fg-header[data-scrolled="true"]::after { opacity: 0; }
.fg-header.is-scrolled,
.fg-header[data-scrolled="true"],
.fg-header[data-menu-open="true"] {
  backdrop-filter: blur(14px) saturate(1.15);
  -webkit-backdrop-filter: blur(14px) saturate(1.15);
}

/* THE PAGE'S TOP CLEARANCE, AND THE ONE PLACE ITS VALUE IS STATED.

   The header is position: fixed, so the first thing in the flow sits under
   it. This spacer is what a page puts there instead, and its height is
   --header-offset, the same token the header's own box is measured with, so
   the gap above a page's first band is settable in tokens.css and nowhere
   else. Changing --header-offset / --header-offset-mobile moves this spacer;
   note that the token has other consumers (the mobile nav's top, in-page
   anchor scroll-margin, the message strip below), so a change to the visible
   gap alone belongs on the height here rather than on the token.

   IT IS UNCONDITIONAL IN THE MARKUP. It used to be printed on
   {% if not page.pre_content %}, which reads as "unless the message strip is
   carrying the clearance instead" and is in fact always false, because the
   messages block always renders a hidden fallback div and the region is never
   empty. Every template now prints the spacer, and the one case where the
   strip does carry the clearance is handled in admin-context.css, on the
   presence of a message rather than on the presence of the region. */
.fg-header-offset { height: var(--header-offset); }

/* --- Utility bar (desktop only) --- */
.fg-header__utility {
  border-bottom: var(--border-hairline-w) solid var(--border-hairline-faint);
}
.fg-header__utility-inner {
  width: 100%;
  max-width: calc(var(--container-max) + (var(--gutter) * 2));
  margin-inline: auto;
  padding-inline: var(--gutter);
  height: var(--header-utility-h);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-6);
}
/* 28px tall, so the hit area clears the 24px minimum. */
.fg-utility-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: var(--header-utility-h);
  padding-inline: 2px;
  background: none;
  border: 0;
  font-family: var(--font-body);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-chip);
  color: var(--text-utility);          /* 5.36:1 base */
  text-decoration: none;
  cursor: pointer;
  transition: var(--t-color);
}
.fg-utility-link:hover { color: var(--text-display); }
.fg-utility-link svg { width: 12px; height: 12px; flex: none; }

/* --- Main row --- */
/* The header's bottom hairline lives here, not on .fg-header: a non-positioned
   child paints above the negative-z scrim layers, so the rule stays visible. */
.fg-header__inner {
  width: 100%;
  max-width: calc(var(--container-max) + (var(--gutter) * 2));
  margin-inline: auto;
  padding-inline: var(--gutter);
  height: var(--header-h);
  display: flex;
  align-items: center;
  gap: clamp(12px, 0.3rem + 1.6vw, 32px);
  box-shadow: 0 1px 0 var(--border-hairline-faint);
}
.fg-header.is-scrolled .fg-header__inner,
.fg-header[data-scrolled="true"] .fg-header__inner,
.fg-header[data-menu-open="true"] .fg-header__inner { box-shadow: 0 1px 0 var(--border-hairline); }

/* --- Wordmark: orange square + Barlow Condensed --- */
.fg-logo {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex: none;
  text-decoration: none;
}
.fg-logo__mark {
  width: 20px; height: 20px;
  flex: none;
  background: var(--accent);
}
.fg-logo__word {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: 15px;
  line-height: 1;
  letter-spacing: var(--ls-wordmark);
  text-transform: uppercase;
  color: var(--text-display);
}
/* Retained so any legacy split-wordmark markup still renders as plain text. */
.fg-logo__t { display: inline; }

/* THE TARGET FLOOR ON THE HEADER WORDMARK. The link is inline-flex round a
   15px image, so its box measured 144x15 in the header and 163x17 in the
   footer: the only recurring target on the site under 24px once the WCAG 2.2
   exemptions are applied. Padding counts toward a target, and so does a
   min-height on a box that already centres its content, so the floor is set
   without moving anything: the image stays vertically centred, the header row
   is 56px and the logo box is 24, so no ancestor changes size. Advisory tier,
   a WCAG 2.2 addition rather than a 2.1 AA criterion.
   THE FOOTER COPY IS LEFT ALONE deliberately. It sits in a stacked brand block
   rather than a row, where a taller box would add space above the address, and
   the advisory finding names the header. */
.fg-header .fg-logo { min-height: 24px; }

/* --- Desktop nav --- */
.fg-nav { display: flex; align-items: center; gap: 2px; }
.fg-nav__item { position: relative; }

.fg-nav__trigger,
.fg-nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 17px 12px;
  background: none;
  border: 0;
  font-family: var(--font-body);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  line-height: 1;
  letter-spacing: 0;
  color: var(--text-nav);              /* 6.27:1 on the solid header */
  text-decoration: none;
  cursor: pointer;
  transition: var(--t-color);
}
.fg-nav__trigger:hover,
.fg-nav__link:hover,
.fg-nav__trigger[aria-expanded="true"] { color: var(--text-display); }

.fg-nav__caret {
  width: 9px; height: 9px;
  flex: none;
  opacity: 0.6;
  transition: transform var(--dur-quick) var(--ease-out-expo), opacity var(--dur-quick) linear;
}
.fg-nav__trigger[aria-expanded="true"] .fg-nav__caret { transform: rotate(180deg); opacity: 1; }

/* Current-page marker. An orange rule as well as a brighter label, so the
   state is never colour-only. */
.fg-nav__trigger[aria-current="page"],
.fg-nav__link[aria-current="page"] { color: var(--text-display); }
.fg-nav__trigger[aria-current="page"]::after,
.fg-nav__link[aria-current="page"]::after {
  content: "";
  position: absolute;
  left: 12px; right: 12px;
  bottom: 8px;
  height: 2px;
  background: var(--accent);
}

/* --- Dropdown card --- */
.fg-megamenu {
  position: absolute;
  top: 100%;
  left: -8px;
  z-index: var(--z-menu);
  padding-top: 4px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: var(--t-panel);
  pointer-events: none;
}
.fg-megamenu--end { left: auto; right: -8px; }
.fg-megamenu[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s;
  pointer-events: auto;
}

.fg-megamenu__panel {
  background: var(--surface-raised);
  border: var(--border-hairline-w) solid var(--border-hairline-strong);
  box-shadow: var(--shadow-menu);
  padding-block: var(--space-2);
  display: grid;
}
.fg-megamenu__panel--1col { grid-template-columns: var(--menu-col); }
.fg-megamenu__panel--2col { grid-template-columns: repeat(2, var(--menu-col)); }
/* Company: DOM order is the pinned spec order and grid-auto-flow: column with
   explicit rows fills top-to-bottom, so visual, tab and mobile-clone order
   all match the spec. */
.fg-megamenu__panel--cols {
  grid-template-columns: repeat(2, 204px);
  grid-template-rows: repeat(4, auto);
  grid-auto-flow: column;
}

.fg-megamenu__group-label {
  grid-column: 1 / -1;
  font-family: var(--font-body);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-muted);            /* 5.31:1 on the dropdown surface */
  padding: var(--space-3) 20px var(--space-2);
}
.fg-megamenu__divider {
  grid-column: 1 / -1;
  height: 1px;
  background: var(--border-hairline);
  margin: var(--space-2) 20px;
}

.fg-menu-item {
  display: block;
  padding: 10px 20px;
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-standard);
}
.fg-menu-item__title {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  line-height: 1.35;
  color: var(--text-primary);          /* 15.47:1 on the dropdown surface */
  transition: var(--t-color);
}
.fg-menu-item__desc {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-2xs);
  line-height: 1.4;
  color: var(--text-muted);            /* 5.31:1 at rest, 4.86:1 on hover */
}
/* Hover lifts the row by white/0.04 (#181b29) and turns the label orange.
     --accent on #181b29  4.56:1  AA
   The dropdown card must stay on --surface-raised for this to hold: the same
   hover over --surface-panel puts the orange label at 4.23:1, which fails. */
.fg-menu-item:hover,
.fg-menu-item:focus-visible { background: rgba(255, 255, 255, 0.04); }
.fg-menu-item:hover .fg-menu-item__title,
.fg-menu-item:focus-visible .fg-menu-item__title { color: var(--accent); }

.fg-menu-item--full { grid-column: 1 / -1; }
.fg-menu-item--feature .fg-menu-item__title { color: var(--accent); }
.fg-menu-item--feature:hover .fg-menu-item__title { color: var(--accent); }
.fg-menu-item--compact { padding-block: 9px; }

/* --- Two-destination product row.
   Each row in the Products menu answers both questions the portfolio gets
   asked: "tell me about this family" and "show me the parts". The title link
   takes the primary destination and keeps the full .fg-menu-item treatment;
   the search link under it is the subordinate second action, set at the
   description's size and colour so the row still has one obvious first click.
   Anchors cannot nest, so .fg-menu-row is a plain wrapper and both links are
   .fg-menu-item, which also keeps them inside the megamenu's arrow-key ring.
   Both rules below are two-class selectors, so source order decides: the
   --action rule comes second and wins on the properties it restates. --- */
.fg-menu-row { display: block; }
.fg-menu-row > .fg-menu-item { padding-block: 10px 2px; }
.fg-menu-row > .fg-menu-item--action {
  display: flex;
  align-items: center;
  gap: 7px;
  padding-block: 2px 9px;
  font-family: var(--font-body);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-medium);
  line-height: 1.4;
  color: var(--text-muted);            /* 5.31:1 on the dropdown surface */
  transition: var(--t-color);
}
.fg-menu-row > .fg-menu-item--action svg { width: 11px; height: 11px; flex: none; opacity: 0.8; }
.fg-menu-row > .fg-menu-item--action:hover,
.fg-menu-row > .fg-menu-item--action:focus-visible { color: var(--accent); }

/* --- Right-hand utilities --- */
.fg-nav-utils {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: auto;
  flex: none;
}
.fg-nav-utils .fg-nav__link { padding-inline: 4px; }

/* CTA discipline. The page's own primary Request a Briefing is on screen at
   the top of a page; the header's copy of it would be the same offer twice in
   one viewport. So the header button waits for the scrolled state, which
   motion.js already sets on #fg-header past 12px. Visibility rather than
   display keeps the button's box in the row, so nothing beside it moves when
   it arrives. Below 1024px the breakpoint block hides it outright and this
   never applies. */
.fg-header .fg-nav-utils .fg-btn {
  opacity: 0;
  visibility: hidden;
  transition: var(--t-panel);
}
.fg-header.is-scrolled .fg-nav-utils .fg-btn,
.fg-header[data-scrolled="true"] .fg-nav-utils .fg-btn {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}

/* 40px target. */
.fg-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px; height: 40px;
  background: none;
  border: var(--border-hairline-w) solid transparent;
  color: var(--text-nav);
  cursor: pointer;
  transition: var(--t-color);
}
.fg-icon-btn:hover { color: var(--text-display); border-color: var(--border-ui); }
.fg-icon-btn svg { width: 16px; height: 16px; }

/* --- Header search.
   The trigger is a plain .fg-icon-btn and it never moves. The field is taken
   out of flow and anchored to the trigger's left edge, so opening it costs
   the row no width: the CTA beside it, the hamburger and the nav all stay
   exactly where they were, and the scrolled-state CTA rule above keeps
   working untouched. What the field covers instead is the tail of the nav,
   which is why it carries the dropdown card's own surface, hairline and
   shadow: over a transparent header on a hero it still has to read as a
   panel. Widths: the open width is a clamp so the field never crowds the nav
   at 1025px and never sprawls at 1440.

   The open transition restates --t-panel by hand rather than using it,
   because the field also needs a width leg and --t-panel spends its third
   slot on transform, which here is the static vertical centring. Same
   durations, same easing, same visibility-after-the-fade trick, same
   transition-delay: 0s on the open state that every panel in this kit uses
   to make the opening direction instant. --- */
.fg-header-search { position: relative; display: inline-flex; align-items: center; flex: none; }

.fg-header-search__field {
  position: absolute;
  top: 50%;
  right: calc(100% + var(--space-2));
  transform: translateY(-50%);
  z-index: var(--z-menu);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 0;
  padding: 0;
  overflow: hidden;
  background: var(--surface-raised);
  border: var(--border-hairline-w) solid var(--border-hairline-strong);
  box-shadow: var(--shadow-menu);
  opacity: 0;
  visibility: hidden;
  transition: width var(--dur-base) var(--ease-out-expo),
              padding var(--dur-base) var(--ease-out-expo),
              opacity var(--dur-base) var(--ease-out-expo),
              visibility 0s linear var(--dur-base);
}
.fg-header-search__field[data-open="true"] {
  width: clamp(200px, 20vw, 320px);
  padding: 0 12px;
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}
.fg-header-search__glyph { width: 14px; height: 14px; flex: none; color: var(--text-muted); }

.fg-header-search__input {
  flex: 1 1 auto;
  min-width: 0;
  height: 36px;
  background: none;
  border: 0;
  font-family: var(--font-body);
  font-size: var(--fs-xs);
  color: var(--text-primary);
}
.fg-header-search__input::placeholder { color: var(--text-muted); opacity: 1; }
.fg-header-search__input:focus { outline: none; }
/* The focus ring belongs on the card, not on a borderless input inside it. */
.fg-header-search__field:focus-within { border-color: var(--accent); }
/* Chrome and Safari draw their own clear button; the field is decorative here. */
.fg-header-search__input::-webkit-search-decoration,
.fg-header-search__input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

.fg-header-search__trigger[aria-expanded="true"] { color: var(--text-display); border-color: var(--border-ui); }

/* --- Hamburger. 40px target, functional border. --- */
.fg-nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 40px; height: 40px;
  background: none;
  border: var(--border-hairline-w) solid var(--border-ui);   /* 3.90:1 */
  cursor: pointer;
  flex-direction: column;
}
.fg-nav-toggle__bar {
  display: block;
  width: 17px; height: 1.5px;
  background: var(--text-display);
  transition: transform var(--dur-base) var(--ease-out-expo), opacity var(--dur-fast) linear;
}
.fg-nav-toggle[aria-expanded="true"] .fg-nav-toggle__bar:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.fg-nav-toggle[aria-expanded="true"] .fg-nav-toggle__bar:nth-child(2) { opacity: 0; }
.fg-nav-toggle[aria-expanded="true"] .fg-nav-toggle__bar:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* --- Mobile panel --- */
.fg-mobile-nav {
  position: fixed;
  top: var(--header-offset-mobile);
  left: 0; right: 0;
  max-height: calc(100vh - var(--header-offset-mobile));
  max-height: calc(100dvh - var(--header-offset-mobile));
  overflow-y: auto;
  overscroll-behavior: contain;
  z-index: var(--z-menu);
  background: var(--surface-raised);
  border-top: var(--border-hairline-w) solid var(--border-hairline);
  padding: var(--space-3) var(--gutter) var(--space-10);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: var(--t-panel);
  display: none;
}
.fg-mobile-nav[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition-delay: 0s;
}

.fg-mobile-nav__section { border-bottom: var(--border-hairline-w) solid var(--border-hairline); }
.fg-mobile-nav__trigger,
.fg-mobile-nav__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  padding: 15px 2px;
  background: none;
  border: 0;
  font-family: var(--font-body);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  text-align: left;
  color: var(--text-muted);            /* 5.31:1 on the panel surface */
  text-decoration: none;
  cursor: pointer;
}
.fg-mobile-nav__link { color: var(--text-primary); }
.fg-mobile-nav__trigger .fg-nav__caret { width: 11px; height: 11px; }
.fg-mobile-nav__trigger[aria-expanded="true"] .fg-nav__caret { transform: rotate(180deg); }
.fg-mobile-nav__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-base) var(--ease-out-expo);
}
.fg-mobile-nav__panel > div { overflow: hidden; }
.fg-mobile-nav__trigger[aria-expanded="true"] + .fg-mobile-nav__panel { grid-template-rows: 1fr; }
.fg-mobile-nav__panel .fg-menu-item { padding: 9px 0 9px var(--space-3); }
.fg-mobile-nav__panel .fg-menu-item__title { font-size: var(--fs-small); }
/* The cloned product rows keep their pairing here too: title, then the quiet
   search link tucked under it. */
.fg-mobile-nav__panel .fg-menu-row > .fg-menu-item { padding: 9px 0 2px var(--space-3); }
.fg-mobile-nav__panel .fg-menu-row > .fg-menu-item--action { padding: 2px 0 10px var(--space-3); font-size: var(--fs-xs); }
.fg-mobile-nav__panel .fg-megamenu__group-label { padding-inline: var(--space-3); }
.fg-mobile-nav__panel .fg-megamenu__divider { margin-inline: var(--space-3); }
.fg-mobile-nav__panel > div > :last-child { margin-bottom: var(--space-3); }

/* Search, first thing in the panel. Built by motion.js, like the utility bar
   and the button pair, so no page carries markup for it. Full width here
   rather than an expanding field: the panel has the room and a tap saved is
   worth more on a phone. */
.fg-mobile-nav__search {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 12px;
  margin-bottom: var(--space-4);
  background: var(--surface-panel);
  border: var(--border-hairline-w) solid var(--border-hairline-strong);
}
.fg-mobile-nav__search:focus-within { border-color: var(--accent); }
.fg-mobile-nav__search svg { width: 15px; height: 15px; flex: none; color: var(--text-muted); }
.fg-mobile-nav__search-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 44px;
  background: none;
  border: 0;
  font-family: var(--font-body);
  font-size: var(--fs-small);
  color: var(--text-primary);
}
.fg-mobile-nav__search-input::placeholder { color: var(--text-muted); opacity: 1; }
.fg-mobile-nav__search-input:focus { outline: none; }
.fg-mobile-nav__search-input::-webkit-search-decoration,
.fg-mobile-nav__search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

/* Cloned utility-bar links. */
.fg-mobile-nav__utilbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
  padding-block: var(--space-4);
  border-bottom: var(--border-hairline-w) solid var(--border-hairline);
}
.fg-mobile-nav__utilbar .fg-utility-link { height: auto; padding-block: 6px; font-size: var(--fs-small); }

.fg-mobile-nav__utils {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

/* --- Entrance cascade.
   The panel's own move is unchanged: --t-panel drops it 8px and fades it over
   --dur-base, on the same duration as the .fg-nav-backdrop fade below, so the
   two still arrive together. What this adds is layering. The five nav
   sections, the cloned utility bar and the button pair slide in from the left
   a beat apart, so the panel reads as a list assembling rather than a slab
   appearing.

   SEVEN SLOTS, 40ms APART, 200ms EACH. The last opens at 240ms and finishes at
   440ms. The 10px slide runs in x while the panel runs in y, the same
   cross-axis rule the homepage stage drawer follows, which is what keeps the
   two moves legible as two moves. Slot 7 is a cap rather than a step: a header
   that grew an eighth section would otherwise walk the tail of the cascade
   past half a second, so everything from the seventh child on shares one
   delay.

   AN ANIMATION, NOT A TRANSITION. motion.js builds this panel's contents on
   the first open and appends them BEFORE it flips data-open, so on that open
   the children have no previous computed style and a transition-delay cascade
   would be skipped entirely. An animation runs whenever its selector starts
   matching, which covers the first open and every one after it identically.
   backwards fill holds the 0 percent state through the delay.

   THE CLOSE IS NOT STAGGERED. Everything hangs off [data-open="true"], so
   dropping the attribute takes the animation with it and the panel fades out
   with its contents composed and at full strength.

   The selector reads the markup contract stated in
   templates/page/_fg2-header.html.twig, which every page template includes:
   .fg-mobile-nav holds a single empty <nav>, and motion.js appends into
   that. --- */
.fg-mobile-nav[data-open="true"] > nav > * {
  animation: fg-mobile-nav-in var(--dur-quick) var(--ease-out-quart) backwards;
}
.fg-mobile-nav[data-open="true"] > nav > :nth-child(1)   { animation-delay: 0s; }
.fg-mobile-nav[data-open="true"] > nav > :nth-child(2)   { animation-delay: 40ms; }
.fg-mobile-nav[data-open="true"] > nav > :nth-child(3)   { animation-delay: 80ms; }
.fg-mobile-nav[data-open="true"] > nav > :nth-child(4)   { animation-delay: 120ms; }
.fg-mobile-nav[data-open="true"] > nav > :nth-child(5)   { animation-delay: 160ms; }
.fg-mobile-nav[data-open="true"] > nav > :nth-child(6)   { animation-delay: 200ms; }
.fg-mobile-nav[data-open="true"] > nav > :nth-child(n+7) { animation-delay: 240ms; }

@keyframes fg-mobile-nav-in {
  0%   { opacity: 0; transform: translate3d(-10px, 0, 0); }
  100% { opacity: 1; transform: none; }
}

/* GUARDS, the same shape as every other one in the kit. No paused rule: this
   is the interface answering a tap, not an ambient loop, and the pause control
   governs loops. Reduced motion drops it outright rather than shortening it,
   because --dur-quick collapses on its own but the seven delays are literals
   and would leave the cascade intact, spread across 240ms with each item
   snapping. animation: none puts every child at its natural state on the first
   frame. The bare html selector outranks the grouped animation rule, which is
   what removes animation-name; the html[data-motion] twin outranks the
   nth-child delay rules themselves. The bare one covers a page where motion.js
   never ran. */
html[data-motion="reduced"] .fg-mobile-nav[data-open="true"] > nav > * { animation: none; }

@media (prefers-reduced-motion: reduce) {
  html .fg-mobile-nav[data-open="true"] > nav > *,
  html[data-motion] .fg-mobile-nav[data-open="true"] > nav > * { animation: none; }
}

/* Breakpoint switch: desktop megamenu above 1024px, panel at or below. */
@media (max-width: 1024px) {
  .fg-header__utility { display: none; }
  /* No utility bar to stand the row off the top here, so the compact row
     takes the breathing room itself: 8px above it and 56 -> 64px of row, with
     the flex centring unchanged so the contents stay centred in the taller
     row. --header-h-mobile was declared and unused; this is its consumer. */
  .fg-header { padding-top: 8px; }
  .fg-header__inner { height: var(--header-h-mobile); }
  .fg-header-offset { height: var(--header-offset-mobile); }
  /* Search goes with them: at this width the field would have to cover the
     wordmark to earn its width, so motion.js puts it at the top of the
     mobile panel instead. */
  .fg-nav,
  .fg-nav-utils .fg-btn,
  .fg-nav-utils .fg-nav__link,
  .fg-header-search { display: none; }
  .fg-nav-toggle { display: inline-flex; }
  .fg-mobile-nav { display: block; }
  .fg-megamenu { display: none; }
}

/* Body scroll lock while the mobile panel is open. */
body[data-nav-open="true"] { overflow: hidden; }

/* --- Backdrop behind the open mobile panel.
   Injected once by motion.js, so none of the pages carry markup for it.
   z-index 590 is chosen against the live stack: every page layer in this set
   tops out at 2, the header is 600 and the panel is 700. So the backdrop
   covers and blurs the whole page while the header row stays crisp, and the
   hamburger inside it stays clickable as the close control. A raw value is
   used because the token scale has no step between --z-sticky (100) and
   --z-header (600). --dur-base collapses to 1ms under reduced motion; the
   explicit rule below removes the transition outright, and the backdrop
   still appears and disappears. --- */
.fg-nav-backdrop {
  position: fixed;
  inset: 0;
  z-index: 590;
  display: none;
  background: rgba(9, 12, 20, 0.58);        /* --surface-base #090c14 at 0.58 */
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard),
              visibility 0s linear var(--dur-base);
}
.fg-nav-backdrop[data-open="true"] {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity var(--dur-base) var(--ease-standard), visibility 0s;
}

/* Present only at the breakpoint that has a panel, matching .fg-mobile-nav. */
@media (max-width: 1024px) {
  .fg-nav-backdrop { display: block; }
}

@media (prefers-reduced-motion: reduce) {
  .fg-nav-backdrop,
  .fg-nav-backdrop[data-open="true"] { transition: none; }
}

@media print {
  .fg-nav-backdrop { display: none !important; }
}

/* --------------------------------------------------------------------------
   11. CTA BAND  ·  global closing component
   -------------------------------------------------------------------------- */

.fg-cta-band {
  position: relative;
  padding-block: var(--section-y-lg);
  background:
    radial-gradient(90% 160% at 88% 12%, rgba(227, 85, 48, 0.08) 0%, rgba(227, 85, 48, 0) 60%),
    var(--surface-base);
  border-top: var(--border-hairline-w) solid var(--border-hairline);
}
/* Over the densest point of that wash (#1a1216):
     --text-display 16.83:1 · --text-muted 5.23:1 · --accent 4.91:1 */
.fg-cta-band__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(28px, 2rem + 3vw, 72px);
  flex-wrap: wrap;
}
.fg-cta-band__copy { flex: 1 1 440px; }
.fg-cta-band__copy .fg-lede { margin-top: var(--space-3); }
.fg-cta-band .fg-btn-group { flex: none; max-width: 100%; }

/* --------------------------------------------------------------------------
   12. FOOTER  ·  compact
   -------------------------------------------------------------------------- */

.fg-footer {
  background: var(--surface-base);
  border-top: var(--border-hairline-w) solid var(--border-hairline);
  padding-top: clamp(48px, 2.6rem + 2.6vw, 72px);
}
.fg-footer__top {
  display: grid;
  grid-template-columns: minmax(220px, 1.6fr) repeat(4, minmax(0, 1fr));
  gap: clamp(28px, 1.6rem + 2.2vw, 48px);
  padding-bottom: clamp(36px, 2rem + 2vw, 56px);
}
.fg-footer__brand { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4); }
.fg-footer__tagline {
  font-size: var(--fs-small);
  line-height: 1.6;
  color: var(--text-muted);            /* 5.57:1 on --surface-base */
  max-width: 30ch;
}

/* The four link columns are direct grid children of __top, so the brand block
   and the columns share one grid. Retained as a wrapper for older markup. */
.fg-footer__cols { display: contents; }

.fg-footer__col-title {
  font-family: var(--font-body);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-muted);            /* 5.57:1 */
  margin-bottom: var(--space-3);
}
.fg-footer__list { display: flex; flex-direction: column; }
/* padding-block gives a 29px hit area on a 14px label. */
.fg-footer__link {
  display: inline-block;
  padding-block: 5px;
  font-size: var(--fs-small);
  line-height: 1.35;
  color: var(--text-nav);              /* 6.27:1 on --surface-base */
  text-decoration: none;
  transition: var(--t-color);
}
.fg-footer__link:hover { color: var(--text-display); }

/* The legal bar is a stack: the copyright and the links on one row, then the
   address and the endorsement line under it. */
.fg-footer__legal {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  /* The extra bottom padding is the clear zone for the fixed motion control,
     which sits bottom-right over the end of the page. */
  padding-top: var(--space-4);
  padding-bottom: calc(var(--space-6) + 48px);
  border-top: var(--border-hairline-w) solid var(--border-hairline);
}
.fg-footer__legal-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}
.fg-footer__copyright { font-size: var(--fs-2xs); color: var(--text-muted); }  /* 5.57:1 */
/* Same size and tone as the copyright, so the whole bar reads as one quiet
   block. The measure is set just past the longer of the two strings, so each
   sits on one line from the tablet width up and stops well short of the full
   width of a wide viewport. address defaults to italic and is reset here. */
.fg-footer__address,
.fg-footer__disclaimer {
  font-size: var(--fs-2xs);
  line-height: 1.5;
  color: var(--text-muted);            /* 5.57:1 */
  max-width: 102ch;
}
.fg-footer__address { font-style: normal; }
.fg-footer__legal-links { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-6); }
/* padding-block gives a 29px hit area on a 12px label. */
.fg-footer__legal-links a {
  display: inline-block;
  padding-block: 6px;
  font-size: var(--fs-2xs);
  line-height: 1.4;
  color: var(--text-muted);            /* 5.57:1 */
  text-decoration: none;
  transition: var(--t-color);
}
.fg-footer__legal-links a:hover { color: var(--text-display); }

@media (max-width: 1024px) {
  .fg-footer__top { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .fg-footer__brand { grid-column: 1 / -1; }
}
@media (max-width: 480px) {
  .fg-footer__top { grid-template-columns: minmax(0, 1fr); gap: var(--space-7); }
  .fg-footer__legal-row { flex-direction: column; align-items: flex-start; }
}

/* --------------------------------------------------------------------------
   13. SCROLL REVEAL
   The hidden state only applies once JS has taken over, so the page stays
   fully readable if scripting never runs.
   -------------------------------------------------------------------------- */

html[data-fg-js="true"] [data-reveal] {
  opacity: 0;
  transform: translate3d(0, var(--reveal-rise), 0);
  transition: opacity var(--dur-reveal) var(--ease-entrance),
              transform var(--dur-reveal) var(--ease-entrance);
  will-change: opacity, transform;
}
html[data-fg-js="true"] [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
  will-change: auto;
}
[data-reveal-delay="1"] { transition-delay: 70ms; }
[data-reveal-delay="2"] { transition-delay: 140ms; }
[data-reveal-delay="3"] { transition-delay: 210ms; }
[data-reveal-delay="4"] { transition-delay: 280ms; }
[data-reveal-delay="5"] { transition-delay: 350ms; }
[data-reveal-delay="6"] { transition-delay: 420ms; }

/* --------------------------------------------------------------------------
   14. MOTION CONTROL
   Fixed, unobtrusive, always reachable, 40px minimum target. aria-pressed
   carries the state. motion.js hides it entirely on a page with no ambient
   motion to control.
   -------------------------------------------------------------------------- */

.fg-motion-toggle {
  position: fixed;
  right: max(var(--space-5), env(safe-area-inset-right));
  bottom: max(var(--space-5), env(safe-area-inset-bottom));
  z-index: var(--z-motion);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-width: 40px;
  min-height: 40px;
  padding: 10px 16px;
  background: rgba(9, 12, 20, 0.9);
  border: var(--border-hairline-w) solid var(--border-ui);   /* 3.90:1 */
  border-radius: 0;
  backdrop-filter: blur(10px) saturate(1.15);
  -webkit-backdrop-filter: blur(10px) saturate(1.15);
  font-family: var(--font-body);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-chip);
  color: var(--text-primary);          /* 16.22:1 on the control fill */
  cursor: pointer;
  opacity: 0.72;
  transition: var(--t-color), opacity var(--dur-quick) linear;
}
.fg-motion-toggle[hidden] { display: none; }
.fg-motion-toggle:hover,
.fg-motion-toggle:focus-visible { opacity: 1; border-color: var(--border-strong); }
.fg-motion-toggle__icon { width: 12px; height: 12px; flex: none; color: var(--accent); }  /* 5.22:1 */
.fg-motion-toggle__icon--play { display: none; }
.fg-motion-toggle[aria-pressed="true"] .fg-motion-toggle__icon--pause { display: none; }
.fg-motion-toggle[aria-pressed="true"] .fg-motion-toggle__icon--play  { display: block; }
.fg-motion-toggle[aria-pressed="true"] { opacity: 1; }

@media (max-width: 640px) {
  .fg-motion-toggle { padding: 10px; }
  .fg-motion-toggle__label { display: none; }
}

/* Global kill switch for every CSS-driven ambient loop. motion.js sets
   data-motion on <html>; JS-driven loops additionally receive their
   registered stop callback. */
html[data-motion="paused"] [data-ambient],
html[data-motion="paused"] .fg-ambient,
html[data-motion="reduced"] [data-ambient],
html[data-motion="reduced"] .fg-ambient {
  animation-play-state: paused !important;
}
html[data-motion="reduced"] [data-ambient],
html[data-motion="reduced"] .fg-ambient {
  animation: none !important;
  transition: none !important;
}

/* --------------------------------------------------------------------------
   15. AMBIENT PRIMITIVES AND BACKGROUND MOTIFS
   All motif layers are decorative and pointer-transparent. Nothing here
   flashes, and nothing carries meaning.
   -------------------------------------------------------------------------- */

@keyframes fg-drift {
  0%   { transform: translate3d(0, 0, 0) scale(1.04); }
  50%  { transform: translate3d(-1.6%, -1.1%, 0) scale(1.08); }
  100% { transform: translate3d(0, 0, 0) scale(1.04); }
}
@keyframes fg-shimmer {
  0%   { opacity: 0.28; }
  50%  { opacity: 0.55; }
  100% { opacity: 0.28; }
}
@keyframes fg-sweep {
  0%   { transform: translateX(-120%); }
  100% { transform: translateX(220%); }
}

.fg-ambient-drift   { animation: fg-drift var(--dur-ambient) var(--ease-in-out) infinite; }
.fg-ambient-shimmer { animation: fg-shimmer calc(var(--dur-ambient) * 0.42) var(--ease-in-out) infinite; }
.fg-ambient-sweep   { animation: fg-sweep calc(var(--dur-ambient) * 0.55) linear infinite; }

/* Hex lattice backdrop. Restrained: 4% opacity, masked to fade at the edges. */
.fg-motif-hex { position: relative; }
.fg-motif-hex > * { position: relative; z-index: 1; }
.fg-motif-hex::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34.64' height='60' viewBox='0 0 34.64 60'%3E%3Cpath d='M17.32 0 L0 10 L0 30 L17.32 40 L34.64 30 L34.64 10 Z M17.32 40 L17.32 60' fill='none' stroke='%237d89a3' stroke-width='1'/%3E%3C/svg%3E");
  background-size: 34.64px 60px;
  opacity: 0.04;
  mask-image: radial-gradient(80% 70% at 50% 0%, #000 0%, transparent 100%);
  -webkit-mask-image: radial-gradient(80% 70% at 50% 0%, #000 0%, transparent 100%);
}
.fg-motif-hex--corner::before {
  mask-image: radial-gradient(52% 80% at 100% 0%, #000 0%, transparent 100%);
  -webkit-mask-image: radial-gradient(52% 80% at 100% 0%, #000 0%, transparent 100%);
}

/* Signal lines: two hairlines across the band, one orange, one neutral. */
.fg-motif-signal { position: relative; }
.fg-motif-signal > * { position: relative; z-index: 1; }
.fg-motif-signal::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(90deg, transparent 0%, rgba(227, 85, 48, 0.26) 34%, rgba(227, 85, 48, 0.26) 66%, transparent 100%),
    linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.08) 22%, rgba(255, 255, 255, 0.08) 78%, transparent 100%);
  background-size: 100% 1px, 100% 1px;
  background-position: 0 28%, 0 72%;
  background-repeat: no-repeat;
}

/* Signal sweep: a thin orange line that lights through a node on hover. It is
   the same vocabulary as the backbone rail, so hovering a node reads as the
   chain answering rather than as a separate effect. */
.fg-signal-sweep { position: relative; }
.fg-signal-sweep::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  height: 1px;
  background: var(--accent-line);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-standard);
  pointer-events: none;
}
.fg-signal-sweep:hover::after,
.fg-signal-sweep:focus-within::after { opacity: 1; }

/* --------------------------------------------------------------------------
   16. MEDIA BAND  ·  full-bleed image well with copy laid over it
   Serves the capability hero and the capability feature band, so all four
   capability instances inherit one behaviour.

   CONTRAST. The scrim holds at least 0.90 alpha across the whole width the
   copy column can occupy. Composited at 0.90 over the LIGHTEST point the
   well can reach (the #1b2233 gradient stop under the 0.10 orange radial,
   so #2f2733) the backdrop is #0d0f17, darker than --surface-base. Measured
   against that worst case:
     --text-display  17.54:1   AAA
     --text-muted     5.46:1   AA
     --accent         5.12:1   AA, and well clear of the 3:1 large-text rule
                                that applies to the highlighted headline word
   Every other point of the well is darker, which only raises the ratio.
   The placeholder label is NOT dimmed by this scrim: it sits at z-index 3,
   above the scrim's z-index 1, and paints its own 0.78 ink backing.
   -------------------------------------------------------------------------- */

.fg-media-band {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background: var(--surface-base);
  min-height: clamp(560px, 36rem + 12vh, 760px);
  padding-top: calc(var(--header-offset) + var(--space-9));
  padding-bottom: clamp(48px, 2.6rem + 3vw, 96px);
}

.fg-media-band__media {
  z-index: auto;
  border: 0;
  align-items: flex-start;
  justify-content: flex-end;
}
.fg-media-band__media .fg-placeholder__label {
  margin: calc(var(--header-offset) + var(--space-5)) var(--gutter) 0 0;
  max-width: 32ch;
}

.fg-media-band__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(90deg,
      rgba(9, 12, 20, 0.96) 0%,
      rgba(9, 12, 20, 0.94) 52%,
      rgba(9, 12, 20, 0.90) 72%,
      rgba(9, 12, 20, 0.55) 100%),
    linear-gradient(0deg,
      rgba(9, 12, 20, 0.55) 0%,
      rgba(9, 12, 20, 0.10) 46%,
      rgba(9, 12, 20, 0.45) 100%);
}

.fg-media-band__inner { position: relative; z-index: 2; width: 100%; }
/* Copy column is capped in px, not ch: ch resolves against the body font,
   which would throttle the condensed display face to about half this width. */
.fg-media-band__copy { max-width: min(100%, 880px); }
/* A long single uppercase word (MICROELECTRONICS, ARCHITECTURES) can outrun a
   390px column at the token size. Capping the headline against the viewport
   keeps every capability title inside its column without touching the scale. */
.fg-media-band__copy .fg-display { font-size: min(var(--fs-display), 15vw); }
.fg-media-band__copy .fg-h1 { max-width: 20ch; font-size: min(var(--fs-h1), 8.2vw); }
.fg-media-band__copy .fg-h2 { max-width: 20ch; font-size: min(var(--fs-h2), 8.2vw); }
.fg-media-band__copy .fg-lede { margin-top: var(--space-5); }
.fg-media-band__copy .fg-btn-group { margin-top: clamp(24px, 1.3rem + 1.2vw, 36px); }

/* Below 900px the copy spans the full width, so the horizontal scrim is
   replaced by a vertical one that holds 0.90 at its lightest point. */
@media (max-width: 900px) {
  .fg-media-band__scrim {
    background: linear-gradient(180deg,
      rgba(9, 12, 20, 0.92) 0%,
      rgba(9, 12, 20, 0.90) 42%,
      rgba(9, 12, 20, 0.97) 100%);
  }
  .fg-media-band { padding-top: calc(var(--header-offset-mobile) + 120px); }
  .fg-media-band__media .fg-placeholder__label {
    max-width: 22ch;
    margin-top: calc(var(--header-offset-mobile) + var(--space-3));
    font-size: var(--fs-3xs);
    padding: var(--space-2) var(--space-3);
  }
}

/* --------------------------------------------------------------------------
   17. TILES, ROWS, ARROW CARDS  ·  capability and drawer furniture
   Tile icons draw in --accent, a non-text UI stroke here: 4.98:1 on the
   card surface against a 3:1 minimum.
   -------------------------------------------------------------------------- */

.fg-tile {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: clamp(18px, 1rem + 0.8vw, 24px);
  background: var(--surface-raised);
  border: var(--border-hairline-w) solid var(--border-hairline);
  transition: var(--t-color);
}
.fg-tile:hover { border-color: var(--border-hairline-strong); }
.fg-tile__icon  { width: 20px; height: 20px; flex: none; color: var(--accent); fill: none; stroke: currentColor; stroke-width: 1.6; }
.fg-tile__body  { display: flex; flex-direction: column; gap: var(--space-2); }
.fg-tile__title {
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  color: var(--text-primary);          /* 15.47:1 on the tile surface */
}
.fg-tile__text { font-size: var(--fs-xs); line-height: 1.55; color: var(--text-muted); }  /* 5.31:1 */

/* Row variant: icon and label on one line, for label-only tiles. */
.fg-tile--row {
  flex-direction: row;
  align-items: center;
  gap: var(--space-3);
  padding-block: clamp(16px, 0.9rem + 0.4vw, 20px);
}
.fg-tile--row .fg-tile__icon { width: 18px; height: 18px; }

/* Link variant. The label is the anchor and a stretched pseudo element makes
   the whole tile the hit area, which is the arrangement the link cards above
   already use, so the two families behave the same to a pointer and to a
   keyboard. The visible ring is drawn on the tile rather than on the words. */
.fg-tile--link { position: relative; cursor: pointer; }
.fg-tile--link:hover,
.fg-tile--link:focus-within { border-color: var(--border-accent); }
.fg-tile--link:hover .fg-tile__title,
.fg-tile--link:focus-within .fg-tile__title { color: var(--accent); }
.fg-tile--link .fg-tile__title a { color: inherit; text-decoration: none; }
.fg-tile--link .fg-tile__title a::after { content: ""; position: absolute; inset: 0; }
.fg-tile--link:has(a:focus-visible) {
  outline: var(--focus-ring-width) solid var(--focus-ring-inner);
  outline-offset: var(--focus-ring-offset);
  box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-ring-outer),
              0 0 0 5px var(--focus-ring-halo);
}
.fg-tile--link .fg-tile__title a:focus-visible { outline: none; box-shadow: none; }

/* Bordered row link. Sits on --surface-base inside a raised panel, so the
   hover tint keeps an orange label at 5.04:1. On --surface-panel the same
   tint would put it at 4.48:1, which fails. */
.fg-row-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 12px 16px;
  background: var(--surface-base);
  border: var(--border-hairline-w) solid var(--border-hairline);
  text-decoration: none;
  transition: var(--t-color);
}
.fg-row-link__label { font-size: var(--fs-small); font-weight: var(--fw-semibold); color: var(--text-primary); transition: var(--t-color); }
.fg-row-link__arrow { width: 14px; height: 14px; flex: none; color: var(--stroke-quiet); transition: transform var(--dur-quick) var(--ease-out-expo), color var(--dur-quick) var(--ease-standard); }
.fg-row-link:hover {
  border-color: var(--border-accent);
  background-image: linear-gradient(var(--accent-tint), var(--accent-tint));
}
.fg-row-link:hover .fg-row-link__label { color: var(--accent); }   /* 5.04:1 */
.fg-row-link:hover .fg-row-link__arrow { color: var(--accent); transform: translateX(3px); }

/* Wide related-item card. Sits on .fg-card .fg-card--link for the panel, the
   stretched hit area and the shared hover treatment. */
.fg-arrow-card__body  { flex-direction: row; align-items: center; gap: var(--space-4); }
.fg-arrow-card__icon  { width: 20px; height: 20px; flex: none; color: var(--accent); fill: none; stroke: currentColor; stroke-width: 1.6; }
.fg-arrow-card__text  { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.fg-arrow-card__arrow {
  width: 15px; height: 15px;
  flex: none;
  color: var(--stroke-quiet);          /* non-text glyph, 4.30:1 on raised */
  transition: transform var(--dur-quick) var(--ease-out-expo), color var(--dur-quick) var(--ease-standard);
}
.fg-card--link:hover .fg-arrow-card__arrow,
.fg-card--link:focus-within .fg-arrow-card__arrow { transform: translateX(3px); color: var(--accent); }

/* Stat block. */
.fg-stat-block { display: flex; flex-direction: column; gap: var(--space-2); padding: clamp(20px, 1.2rem + 1vw, 30px); background: var(--surface-raised); }
.fg-stat-block__label { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text-primary); }
.fg-stat-block__note  { font-size: var(--fs-3xs); color: var(--text-muted); }

/* Anchored sections clear the fixed header when jumped to.
   :where() keeps the specificity at zero so nothing has to fight it. */
:where([id]) { scroll-margin-top: calc(var(--header-offset) + var(--space-5)); }
@media (max-width: 1024px) {
  :where([id]) { scroll-margin-top: calc(var(--header-offset-mobile) + var(--space-5)); }
}

/* --------------------------------------------------------------------------
   18. PRINT  (documents get a docx at assembly; this keeps a sane fallback)
   -------------------------------------------------------------------------- */

@media print {
  .fg-header, .fg-mobile-nav, .fg-motion-toggle, .fg-skip-link { display: none !important; }
  html[data-fg-js="true"] [data-reveal] { opacity: 1 !important; transform: none !important; }
}

/* --------------------------------------------------------------------------
   19. NARROW-DESKTOP HEADER FIT

   Five nav items, a Contact link and the primary CTA sit on one 56px row.
   The v2 header is light enough (13px labels, a 20px logo mark, a compact
   button) that the row never runs out of room. Measured free space between
   the nav and the right-hand utilities, against a 27px working minimum:
   97px at 1025px, 191px at 1119px, 113px at 1120px, 271px at 1280px.
   No trigger wraps at any of those widths and the row itself never overflows.

   One tier, below 1120px, tightens the row a little further so the labels
   keep breathing room from the wordmark on a small laptop. Above that the
   fluid gap handles it and nothing steps.

   nowrap on the triggers is deliberate. Without it a tight header quietly
   breaks the labels onto two lines instead of holding the row.
   -------------------------------------------------------------------------- */

@media (min-width: 1025px) {
  .fg-nav__trigger,
  .fg-nav__link { white-space: nowrap; }
}

@media (min-width: 1025px) and (max-width: 1119px) {
  .fg-header__inner { gap: 12px; }
  .fg-nav { gap: 0; }
  .fg-nav__trigger,
  .fg-nav__link { padding-inline: 8px; gap: 4px; }
  .fg-nav__trigger[aria-current="page"]::after,
  .fg-nav__link[aria-current="page"]::after { left: 8px; right: 8px; }
  .fg-nav__caret { width: 8px; height: 8px; }
  .fg-nav-utils { gap: var(--space-2); }
  .fg-nav-utils .fg-btn--sm { --btn-px: 16px; }
}

/* --- Real Frontgrade logo (fg2/images/frontgrade-logo.svg, white + #E35530 fills, 730x76) --- */
.fg-logo__img { height: 15px; width: auto; display: block; }
.fg-footer__brand .fg-logo__img { height: 17px; }


/* --------------------------------------------------------------------------
   20. WEBFORMS ON THE DARK GROUND

   WHAT THIS SECTION IS FOR. The site renders the same webform module three
   different ways, and two of the three are unreadable. Measured at 1280 on the
   running site before this section:

     page                    label colour     ground          ratio
     /contact-us-form        rgb(232,234,237) rgb(19,25,40)   14.55:1   ok
     /media-inquiry          rgb(18,18,18)    rgb(9,12,20)     1.04:1   fails
     /report-website-issue   rgb(18,18,18)    rgb(9,12,20)     1.04:1   fails
     /ADEPT                  rgb(18,18,18)    rgb(9,12,20)     1.04:1   fails
     /product/DDR4           rgb(18,18,18)    rgb(14,18,32)    1.00:1   fails
     /product/rpmg1-svpx     rgb(18,18,18)    rgb(14,18,32)    1.00:1   fails

   The select elements on the three middle pages measured rgb(0,0,0) on a
   transparent background, 1.07:1, so the chosen value was invisible too.

   WHY ONE PAGE ALREADY WORKED, AND WHY THAT IS THE BUG. The fg2 layer does
   carry a correct dark form skin, but it is written twice and both copies are
   anchored on a wrapper class rather than on the form:

     interior.css  #block-frontgrade-content .fg-interior .file-attachments__form-embed label
     product.css   .fg-product__docs .file-attachments__form-embed label

   .file-attachments__form-embed is the wrapper the file-attachment component
   puts round an embedded form. /contact-us-form is embedded that way and picks
   the skin up; /media-inquiry, /report-website-issue and /ADEPT embed their
   form through a plain webform paragraph, so no wrapper, no skin, and the
   site's own component.css keeps its light-page defaults. The form is the same
   component on every one of those pages, so the form is what the skin should
   name. That is what this section does, once, sitewide.

   THE SCOPE IS THE DARK GROUND AND NOTHING ELSE. main:not(.fg2-legacy-skin)
   is the whole of the guard. legacy-skin.css puts .fg2-legacy-skin on <main>
   for every route that still renders on the white legacy ground, and those
   pages carry webforms too (/user/login, /form/*, the unconverted product
   bundles). Light labels there would repeat this defect with the colours the
   other way up, so those pages keep the treatment legacy-skin.css already
   gives them.

   THE ID IN THE SELECTOR IS NOT DECORATION, for the reason interior.css states
   at its own form band: the site anchors every webform rule on
   #block-frontgrade-content, which scores 1-1-0, and a selector built from
   classes alone cannot outrank it whatever order the aggregator emits. The
   strongest arm that has to be beaten is

     #block-frontgrade-content .webform-submission-form
       .form-actions:not(.webform-progress ~ .form-actions) input        (1,4,1)

   so the submit rule below scores (1,4,2). The label, field and control rules
   beat (1,2,1) at (1,2,2) and (1,3,2). The id is matched, never written.

   THIS ALSO WINS OVER interior.css AND product.css BY ONE ELEMENT, so on
   /contact-us-form and the product pages the values below are the ones that
   land rather than theirs. That is deliberate and it is why the values are
   copied from those files rather than chosen: --text-primary at --fs-small and
   --fw-medium is exactly what interior.css:1847 already sets, so the page that
   was already right renders identically and the other five join it. The two
   older blocks are left in place; they are now agreeing duplicates rather than
   the only copy, and removing them belongs with the templates they were
   written for.
   -------------------------------------------------------------------------- */

/* ---- Labels, legends and descriptions ----------------------------------- */

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form label,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .form-item__label {
  color: var(--text-primary);            /* 14.55:1 on --surface-panel, 16.22:1 on --surface-base */
  font-size: var(--fs-small);
  font-weight: var(--fw-medium);
}

/* A legend names a group rather than a field, so it takes the colour and keeps
   whatever size its own form gives it. Forcing the label size here would
   flatten a section heading into a field label sight unseen. */
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form legend,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .fieldset-legend {
  color: var(--text-primary);
}

/* The required marker keeps the one accent, as the interior band already does. */
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .form-required::after {
  color: var(--accent);
}

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .description,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .form-item__description,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .webform-element-description {
  color: var(--text-muted);              /* 5.57:1 on --surface-base, 4.99:1 on --surface-panel */
  font-size: var(--fs-small);
}

/* ---- Fields -------------------------------------------------------------
   The three unskinned forms dropped white fields onto the dark page and left
   their select values at rgb(0,0,0) on a transparent ground. Both go to the
   same well the skinned form already uses.                                  */

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="text"],
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="email"],
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="tel"],
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="url"],
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="number"],
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="search"],
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="date"],
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form select,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form textarea {
  background-color: var(--surface-raised);
  border: var(--border-hairline-w) solid var(--border-ui);   /* 3.90:1 on --surface-base */
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--fs-body);
}

/* A select paints its own list with the operating system's colours, so the
   option rows are named as well as the closed control. Without this a dark
   value sits on a dark row in the open list on the platforms that honour it. */
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form select option {
  background-color: var(--surface-raised);
  color: var(--text-primary);
}

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input::placeholder,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form textarea::placeholder {
  color: var(--text-secondary);
  opacity: 1;
}

/* ---- Checkbox and radio -------------------------------------------------
   THE CONTROL IS THE INPUT AGAIN, NOT A DRAWING BESIDE IT.

   component.css hides the real control off-screen
   (position:absolute; left:-10000px; 1x1; clip:rect(0,0,0,0)) and draws a
   substitute out of two pseudo-elements on the label: a 1.125em white box with
   a black border in ::before, and a black square in ::after whose opacity goes
   to 1 under `input:checked + label`. Measured on /contact-us-form: the box
   drew at 15.75px and on /product/DDR4 at 18px, both under the 24px minimum,
   both in white-and-black on a dark form, and the real input sat at x = -10001
   so its focus ring painted off-screen. A keyboard visitor arriving at the
   required consent control saw nothing move.

   Restoring the input is the smaller and the more correct repair. It carries
   the state natively, it keeps the label's `for` association untouched, it
   answers Space, it is exposed to assistive tech without help, it survives
   forced-colors mode, and it takes the global focus ring from section 4 with
   no component-level focus rule, which is what the token contract requires.
   The substitute is retired inside this scope only, so the same markup on a
   legacy-ground page keeps the drawing it has today.                        */

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .js-form-type-checkbox,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .js-form-type-radio {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
}

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .js-form-type-checkbox label::before,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .js-form-type-checkbox label::after,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .js-form-type-radio label::before,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .js-form-type-radio label::after {
  content: none;
}

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="checkbox"],
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="radio"] {
  /* Declared on the rule rather than added to the token contract, the way
     .fg-btn declares --btn-py. 24px is the WCAG 2.5.8 minimum target size and
     the control is the smaller half of the target here: the label is
     associated and clickable, so the real target is the box plus the sentence. */
  --fg-choice-size: 24px;
  position: static;
  left: auto;
  clip: auto;
  clip-path: none;
  overflow: visible;
  flex: none;
  width: var(--fg-choice-size);
  height: var(--fg-choice-size);
  min-width: var(--fg-choice-size);
  margin: 0;
  padding: 0;
  text-indent: 0;
  -webkit-appearance: none;
  appearance: none;
  background-color: var(--surface-raised);
  background-position: center;
  background-repeat: no-repeat;
  border: 2px solid var(--border-ui);    /* 3.90:1 on --surface-base, 3.50:1 on --surface-panel */
  cursor: pointer;
}

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="radio"] {
  border-radius: var(--radius-pill);
}

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="checkbox"]:hover,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="radio"]:hover {
  border-color: var(--border-strong);
}

/* THE MARK IS DRAWN, NOT FETCHED. The tick is an inline SVG carried in this
   file as a data URI, so the control costs no request and cannot render as a
   broken image. The stroke is written as %23090c14 because a data URI is a
   string and cannot read a custom property; that literal is --text-on-accent,
   and --text-on-accent on --accent measures 5.22:1, the same pair the primary
   button uses. */
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="checkbox"]:checked {
  background-color: var(--accent);
  border-color: var(--accent);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-6.5' fill='none' stroke='%23090c14' stroke-width='2.2' stroke-linecap='square'/%3E%3C/svg%3E");
  background-size: 16px 16px;
}

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input[type="radio"]:checked {
  background-color: var(--accent);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 3px var(--surface-raised);
}

/* ---- Submit -------------------------------------------------------------
   ONE BUTTON, AND IT IS THE KIT'S PRIMARY. Measured before this rule the same
   control drew at three heights and two fills: h 20 with a transparent
   background on the three paragraph-embedded forms, h 28 with a 3px radius on
   the two product forms, and h 44 in orange with a white label on the four
   skinned forms, where white on --accent measures 3.74:1 and fails. The values
   below are .fg-btn.fg-btn--primary read off section 5, which composes to
   40px (12 + 12 padding, 14px label at leading none, 1px border top and
   bottom) and carries --text-on-accent on --accent at 5.22:1.

   THE PADDING CARRIES !important AND THE REST DOES NOT. styles/dist/
   file-attachments.css sets `padding: 5px 20px !important` on this control at
   (0,3,1); an important declaration is only answered by another one, and that
   single property is the whole reason the product forms draw at 28px. Every
   other property here wins on specificity alone and is left normal. */

main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .form-actions input.form-submit,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .form-actions button.form-submit,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input.form-submit,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form button.form-submit,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input.webform-button--submit,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form button.webform-button--submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;
  padding: 12px 26px !important;
  background-color: var(--accent);
  color: var(--text-on-accent);          /* 5.22:1 on --accent */
  border: var(--border-hairline-w) solid var(--accent);
  border-radius: var(--fg-control-radius);
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-weight: var(--fw-semibold);
  line-height: 1;
  letter-spacing: var(--ls-btn);
  text-transform: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: var(--t-color), transform var(--dur-fast) var(--ease-out-quart);
}

/* The fill does not darken on hover, for the reason section 5 gives: a darker
   fill under a near-black label would drive the measured 5.22:1 down. */
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form .form-actions input.form-submit:hover,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input.form-submit:hover,
main:not(.fg2-legacy-skin) #block-frontgrade-content .webform-submission-form input.webform-button--submit:hover {
  background-color: var(--accent);
  color: var(--text-on-accent);
  border-color: rgba(255, 255, 255, 0.72);
  transform: translateY(-1px);
}


/* ==========================================================================
   21. LEGACY COMPONENT COLOUR ON THE DARK GROUND

   WHAT THIS SECTION IS FOR. page.html.twig puts the redesigned chrome on every
   route, and the converted templates put a dark band under content that is
   still built out of the site's own paragraph components. Those components
   carry colours chosen for a white page, and the site's own stylesheets state
   them on selectors the fg2 layer never answered. Measured at 1440 on the
   running site before this section, with alpha compositing and gradient stops:

     component / selector                    colour    ground     ratio
     body p  in .component-file-attachments  #121212   #090c14    1.04:1
     .component-text__inner p / li / h2      #000000   #0e1220    1.12:1
     body a  (views tables, ctas, tablesaw)  #303030   #090c14    1.48:1
     body a  in a webform description        #303030   #0e1220    1.41:1
     an editor inline colour on a bio        #000000   #090c14    1.07:1
     .fg-color-orange                        #ce401c   #0e1220    3.88:1
     .teaser-card cta text                   #ce401c   #131928    3.65:1
     --text-muted over a legacy zebra row    #7d89a3   #f7f7f7    3.28:1
     --text-primary on a legacy white panel  #e8eaed   #ffffff    1.21:1
     --text-muted   on a legacy grey panel   #7d89a3   #e8e8e8    2.87:1

   THERE ARE TWO DEFECTS HERE AND THEY PULL OPPOSITE WAYS. The first seven rows
   are a dark colour left on a dark ground. The last three are the reverse: the
   fg2 text tokens are correct for the band, and a legacy component has painted
   a LIGHT panel underneath them. A single rule cannot answer both, so the
   section is split: 21.1 to 21.5 return legacy text to the band's own colour,
   and 21.6 rebinds the text tokens inside the two components that bring their
   own light panel.

   THE SCOPE IS THE DARK GROUND AND NOTHING ELSE, exactly as section 20 states
   it: main:not(.fg2-legacy-skin) is the whole of the guard, and the pages that
   still render on the white legacy ground keep the treatment legacy-skin.css
   gives them. The id in the selector is matched, never written, and it is
   there for the reason section 20 gives: the site anchors its own rules on
   #block-frontgrade-content, which scores (1,1,0), and a selector built from
   classes alone cannot outrank it whatever order the aggregator emits.
   -------------------------------------------------------------------------- */

/* ---- 21.1  Legacy table zebra rows -------------------------------------

   component.css paints every even table row #f7f7f7, at

     body table tr:nth-child(2n)                                     (0,1,3)
     body .component-table table .tablesaw tr:nth-child(2n)          (0,2,3)

   which puts a white stripe through a table sitting on a dark band. That is
   what drives --text-muted to 3.28:1 and --fg-link to 3.49:1 on those rows:
   the text is right and the ground is wrong. The stripe moves onto the fg2
   surface ladder instead, so the whole table reads on one system. Measured on
   the repainted row: --text-muted 5.31:1, --text-primary 15.47:1,
   --fg-link 4.98:1, --border-ui 3.72:1.

   The catalog's own product table is untouched: product-table.css paints its
   stripe on the td rather than the tr and scores (0,4,2), which lands on top
   of this. Its light-ground twin is untouched too, being outside the guard. */

main:not(.fg2-legacy-skin) table tr:nth-child(2n),
main:not(.fg2-legacy-skin) .component-table table .tablesaw tr:nth-child(2n) {
  background-color: var(--surface-raised);
}

/* ---- 21.2  Legacy body text inside a legacy component ------------------

   base.css sets `body p { color: #121212 }` at (0,0,2) and component.css sets
   `.component-text__inner p, .component-text__inner li { color: #000 }` at
   (0,1,1). Neither is answerable by inheritance, so every paragraph in a
   legacy component on a dark band renders at 1.04:1 or 1.12:1.

   THE VALUE IS `inherit` RATHER THAN A TOKEN, deliberately. These components
   sit in bands that already carry the right colour: --text-muted on an
   interior body, --text-primary inside a card, #e8e8e8 inside a legacy
   .theme-dark section. Naming a token here would flatten those three into one
   and would fight the component's own dark theming. Inheriting takes whatever
   the band already resolved and measured.

   THE FAMILY LIST IS THE EVIDENCE, not a guess: every wrapper below is one the
   sweep recorded a failing node inside. They are all legacy class names, so no
   rule here can reach fg2 markup, which is why the list can be broad without
   being risky.

   TABLE CELLS ARE NOT IN THE ELEMENT LIST, and the omission is measured. No
   td or th failed with a legacy colour: the cells that failed were carrying
   the fg2 token already and failed on their ground, which 21.1 answers. Cells
   are also where the site's Word and Excel pastes live, and those bring their
   own palette: /gan-converters carries a pasted table whose cells declare
   background-color inline at hsl(0,0%,90%) and hsl(0,0%,100%) with the legacy
   dark ink over them, a combination that already passes. Handing those cells
   the band's light text put 102 nodes at 2.81:1 and 3.51:1 in the first pass
   of this section. They keep their own colour instead, and anything nested
   inside them inherits it.

   THE SAME REASONING PUTS :not([style*="background"]) ON THE ELEMENT LIST. An
   element that declares its own background inline has brought a ground with
   it, and the band's colour is not measured against that ground.          */

main:not(.fg2-legacy-skin) :is(
  .component-text,
  .component-text__inner,
  .component-file-attachments,
  .component-table,
  .component-two-columns,
  .component-styled-list,
  .component-switchback__text,
  .file-attachments,
  .tablefield-wrapper,
  .views-element-container,
  .paragraph-drupal-block
) :is(p, li, dd, dt, blockquote, figcaption, strong, em, b, i, small, h1, h2, h3, h4, h5, h6):not([style*="background"]) {
  color: inherit;
}

/* The switchback heading nests a plain div inside the h2, and the heading arm
   above stops at the h2. */
main:not(.fg2-legacy-skin) .component-switchback__heading,
main:not(.fg2-legacy-skin) .component-switchback__heading > div {
  color: inherit;
}

/* ---- 21.3  Legacy links on the dark ground -----------------------------

   base.css sets `body a { color: #303030 }` at (0,0,2), which lands on every
   link the fg2 layer has not claimed: a views table cell, a table sort header,
   an interior call to action, a tablesaw download, the terms link inside a
   webform description. Measured 1.48:1 on --surface-base and 1.41:1 on
   --surface-raised.

   THE FIRST ARM MATCHES ONLY CLASSLESS LINKS. a:not([class]) cannot reach a
   button, a menu item, a card link or any other fg2 control, all of which
   carry a class, so the rule cannot disturb a colour the design layer set. The
   named arms below it are the two legacy link classes the sweep recorded.

   --fg-link is --accent: 5.22:1 on --surface-base, 4.98:1 on --surface-raised
   and 4.68:1 on --surface-panel, with the underline that section 5 requires
   carrying the distinction from the surrounding copy.                     */

main:not(.fg2-legacy-skin) a:not([class]),
main:not(.fg2-legacy-skin) a.arrow-link,
main:not(.fg2-legacy-skin) a.tablesaw-cell-content {
  color: var(--fg-link);
}

/* ---- 21.4  The legacy second orange on the dark ground ------------------

   base.css and component.css carry #ce401c, a second orange the token
   contract does not have. On the dark ground it measures 3.88:1 on
   --surface-raised and 3.65:1 on --surface-panel as normal text, both below
   4.5:1. The fix is the system's one orange rather than a third value:
   --accent measures 4.98:1 and 4.68:1 on the same two surfaces.           */

main:not(.fg2-legacy-skin) .fg-color-orange,
main:not(.fg2-legacy-skin) :is(
  .teaser-card__text,
  .teaser-card-cta-text,
  .component-teaser-cards__teaser-card-cta-text,
  .news__teaser-card-cta-text,
  .leadership__teaser-card-cta-text,
  .views-row__teaser-card-cta-text
) {
  color: var(--accent);
}

/* THE HOVER STATE IS PART OF THE SAME MEASUREMENT and it needs its own value.
   interior.css lifts a teaser card and a link-list row to --surface-elevated
   while it is hovered or focused, and section 3 of tokens.css forbids --accent
   as small text on exactly that surface, where it measures 4.46:1. The legacy
   value it replaced measured 3.48:1 there, so the rule above improves the
   hover state without closing it.

   The contract already names the remedy for accent text that has to sit on
   --surface-elevated: take --fg-link-hover, which is --text-display and
   measures 15.28:1 on that surface, 16.06:1 on --surface-panel and 17.08:1 on
   --surface-raised. The underline and the weight still carry the distinction,
   so nothing about how the row reads as a link depends on the hue.           */

main:not(.fg2-legacy-skin) :is(
  .teaser-card__link,
  .component-teaser-cards__link,
  .component-link-list__link
):is(:hover, :focus-visible) :is(
  .teaser-card__text,
  .teaser-card-cta-text,
  .component-teaser-cards__teaser-card-cta-text,
  .news__teaser-card-cta-text,
  .leadership__teaser-card-cta-text,
  .views-row__teaser-card-cta-text
) {
  color: var(--fg-link-hover);
}

/* ---- 21.5  Editor colour set inline, inside dark prose -----------------

   One leadership bio carries style="color:black" on four spans, pasted in with
   the copy, measuring 1.07:1 on --surface-base. An inline declaration outranks
   every stylesheet, so !important is the only thing that answers it, and the
   scope is kept to elements that carry an inline colour inside a prose wrapper
   the fg2 layer owns.

   ANY editor colour is neutralised here, not only black. A colour chosen
   against a white page carries no guarantee at all on a near-black one, and
   the band's own colour is the one that was measured. Where an editor needs
   emphasis the markup already has strong and em, both of which keep working. */

main:not(.fg2-legacy-skin) :is(
  .fg-prose,
  .fg-bio__prose,
  .fg-interior__body,
  .fg-article__body,
  .fg-catalog__prose,
  .component-text__inner
) :is(span, font, div, p, li, td, th)[style*="color"]:not([style*="background"]) {
  color: inherit !important;
}

/* ---- 21.6  Legacy components that bring their own light panel ----------

   Two components paint a light panel and then let the band's text colour ride
   over it: .component-card-grid paints #ffffff and .component-logo-grid paints
   #e8e8e8. The fg2 tokens are correct for the band and wrong for the panel,
   measuring --text-primary at 1.21:1 and --text-muted at 2.87:1.

   THE PANEL IS KEPT AND THE TEXT MOVES. Repainting the panel dark is the other
   way to close it, and it is the wrong way: the logo grid exists to show
   partner marks drawn for a light background, and the card grid's own cards
   are dark panels sitting inside the light one, so flipping the section would
   invert them too. Rebinding the text tokens on the panel's own text block
   leaves the cards, which are siblings of that block, exactly as they are.

   REBINDING RATHER THAN RESTATING. The rules that set these colours live in
   interior.css and resolve var(--text-muted) in the element's own context, so
   redefining the custom property on the light block is enough and no
   specificity contest is needed. Measured after the rebind:
     --text-ink-on-light   on #ffffff 16.46:1 · on #e8e8e8 13.44:1
     --text-muted-on-light on #ffffff  7.46:1 · on #e8e8e8  6.09:1        */

main:not(.fg2-legacy-skin) :is(
  .card-grid__text-content,
  .component-card-grid__text-content,
  .component-logo-grid__text
) {
  --text-display:   var(--text-ink-on-light);
  --text-primary:   var(--text-ink-on-light);
  --text-muted:     var(--text-muted-on-light);
  --text-secondary: var(--text-muted-on-light);
}

/* ---- 21.7  Exposed-form labels on the dark ground ----------------------

   Section 20 puts the dark form skin on .webform-submission-form. A view's own
   exposed filter form is the same widget under a different wrapper, and
   component.css keeps its light-page label colour there: #121212 measured at
   1.04:1 on --surface-base. The strongest legacy arm is

     #block-frontgrade-content .views-exposed-form .js-form-type-textfield
       label                                                          (1,3,1)

   so the rule below scores (1,4,1) on the same shape section 20 uses.
   --text-primary measures 16.22:1 on --surface-base.                      */

main:not(.fg2-legacy-skin) #block-frontgrade-content .views-exposed-form .js-form-item label,
main:not(.fg2-legacy-skin) #block-frontgrade-content .views-exposed-form .form-item label {
  color: var(--text-primary);
}

/* ---- 21.8  A table header carrying an inline accent fill ---------------

   component.css gives .component-text__inner .table-wrapper th white text on
   #ce401c, which measures 4.80:1. Where an editor has overridden the fill
   inline to the brand accent the same white measures 3.74:1, which is the pair
   section 2 of tokens.css names as never used. The rule answers only the
   inline-accent case and takes --text-on-accent, measured 5.22:1 on --accent.
   It carries no dark-ground guard because the th paints its own fill and the
   measurement is the same on either ground.

   The durable fix is editorial, in the body field that carries the inline
   style; this holds the criterion until that happens.                     */

main .table-wrapper :is(th, td)[style*="e35530" i],
main .component-text__inner :is(th, td)[style*="e35530" i] {
  color: var(--text-on-accent);
}
