/* components.css — Design Kit v2 §04 patterns. Every value traces to tokens.css. */

/* ============================================================
   4.1 — Header & categorized nav. One panel shape: --descriptive
   (About, Brands). Expertise and Responsibility are direct links,
   no chevron. The --anchors panel shape went out with
   Responsibility's dropdown on 2026-08-14. Below 900px: a
   content-height drop-down (not a full-height sheet — that went on
   2026-08-21) of 56px divided rows, with the toggle at the right
   gutter. This is the ONLY home for .site-header / .site-nav rules —
   do not reintroduce them in base.css.

   2026-08-17, maintainer: ≥900px the header is a FLOATING ISLAND,
   not a full-bleed bar. The old solid-white bar with a hairline
   bottom border is gone above 900px; it survives unchanged in the
   max-width:899px block below, because a floating bar is wrong on a
   phone and the drawer is pinned to the header's box.

   Two things are load-bearing here:

   1. THE BAND HEIGHT IS A SHARED NUMBER — four rules in pages.css are
      derived from it, and they are not optional.

      >=900px: island 72px + 8px margin-block = an 88px band.
               island TOP edge  =  8px
               island BOTTOM edge = 80px  -> .tabbar { top: 80px }
               band height      = 88px  -> .hero margin-top: -88px
                                           and +88px of hero padding-top
               80 + 69px tabbar = 149px -> :target scroll-margin-top
      <900px:  solid full-bleed bar, 72px, island flush (margin 0).
               72 + 1px border   = 73px
               73 + 69px tabbar  = 142px -> :target scroll-margin-top
                                            .tabbar { top: 72px }

      Grown from a 56px island / 72px band on 2026-08-17 for more room
      around the logo, links and CTA. If you resize it again, re-derive
      ALL FOUR pages.css numbers or the Responsibility tabbar floats
      over open space and the homepage hero mis-registers by the delta.
      Measure the real boxes with CDP getBoundingClientRect; do not
      estimate.
   2. The island is SOLID WHITE and does not change. It is opaque, so
      nothing scrolls through it and it looks the same on every page
      and at every scroll position. It went through a translucent,
      blends-until-hovered phase during the 2026-08-17 rebuild; that
      was removed the same day by request. See the rule itself before
      reinstating any of it.

   There is no hover, focus or open state on the bar itself. The only
   hover affordance in the header is the per-link pill on
   .site-nav__link.
   ============================================================ */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: transparent;
  min-height: 88px; display: flex; align-items: center;
}
/* The logo link. `display: flex` so the <img> inside is a flex item rather
   than an inline one: as an inline replaced element it sits on the text
   baseline, and the line box reserves descender space under it, leaving a
   few pixels of dead height inside the <a>. That padded the anchor's hit
   area below the mark and offset it very slightly against the nav items it
   is centred with. Flex has no baseline to sit on, so the box wraps the
   image exactly.

   `inline-flex` would fix the same thing, but this is a flex item of
   .site-header__inner already, so its outer display is blockified either
   way and `flex` is the honest spelling of what it computes to. */
/* min-height, not height: the <img> inside sets the visual size (36px) and
   must not stretch. This gives the anchor a 44px hit box around it without
   changing what is drawn. 2026-08-24. */
.site-header__logo { display: flex; align-items: center; min-height: 44px; }
.site-header__inner {
  display: flex; align-items: center; gap: var(--space-8);
  width: 100%; min-height: 72px; margin-block: var(--space-2);
  /* --container-nav (1000px) overrides the 1200px .container the element
     also carries, so the pill is narrower than the content beneath it.
     Radius stays --radius-pill, i.e. a true stadium. GoldKit's literal
     28px on their ~80px bar is a rounded rectangle, not a stadium; on the
     56px island this started at, 28px happened to be exactly half the
     height and so came out as one. The island is 72px now, and the stadium
     is kept on purpose — it is the shape the design was signed off on, not
     an accident of the old height. */
  max-width: var(--container-nav);
  border-radius: var(--radius-pill);
  /* SOLID WHITE, ALWAYS — one static appearance, no state changes
     (maintainer, 2026-08-17: "just keep the navbar white").

     What was here before, so it is not reintroduced by accident: the pill
     was translucent at rest and turned white on :hover, :focus-within and
     :has([data-open]). That whole mechanism is deliberately gone. Three
     things went with it and none are oversights —
       * the translucent background: it was tinted --neutral-50 (#FAF9F7),
         a warm cream, which is what read as a yellow cast;
       * backdrop-filter: pointless behind an opaque surface, and it was
         only ever there to keep links legible while page content scrolled
         under a see-through bar. Nothing shows through now;
       * the transition: nothing changes, so there is nothing to animate.

     The hairline and shadow are now permanent rather than hover-only, so
     the pill still reads as a floating object on the light pages where a
     white-on-#FAF9F7 shape would otherwise have almost no edge.

     The shadow is the neutral --e2, NOT --e-gold-1. A gold-tinted shadow
     is a warm halo around the bar, which is the same complaint that
     removed the warm fill; permanent gold would have been more visible
     than the hover-only gold ever was. --e-gold-1 is now unused;
     --e-gold-2/3 still drive the CTA. */
  background-color: #fff;
  border: 1px solid var(--neutral-200);
  box-shadow: var(--e2);
}

/* flex: 1 so .site-nav now spans the space between the logo and the right
   edge. It gained that job on 2026-08-21 when .site-header__utility moved
   inside it (see nav.njk) — .site-header__inner used to lay out three
   children (logo / nav / utility) and now lays out two. */
.site-nav { display: flex; align-items: center; flex: 1; }
/* >=900px this is simply the row that .site-header__inner used to be, which
   is why the desktop bar did not move: same flex axis, same --space-8 gap,
   and the utility keeps its own `margin-left: auto`. It only becomes a
   distinct object below 900px, where it is the drop-down panel. */
.site-nav__drawer { display: flex; align-items: center; flex: 1; gap: var(--space-8); }
/* gap drops from --space-6 to --space-1 because each link now carries
   --space-3 of its own inline padding to draw its hover pill: 4px gap
   + 2×12px padding ≈ the 24px of visual separation this had before. */
.site-nav__list { display: flex; gap: var(--space-1); list-style: none; margin: 0; padding: 0; }
.site-nav__item { position: relative; }
.site-nav__link {
  background: none; border: 0; font: var(--type-h4); color: var(--ink-900);
  /* 56px, not the 44px control minimum: it scales the hover pill to the
     72px island, and it makes the leftover space above and below the link
     exactly --space-2, which is what the dropdown offset below is built
     on. Island content box is 70px (72 - 2 x 1px border), so the link
     centres at (70 - 56) / 2 = 7px, landing its bottom edge 8px above the
     island's. Changing this height moves the dropdown. */
  min-height: 56px; display: inline-flex; align-items: center;
  padding-inline: var(--space-3); border-radius: var(--radius-md);
  text-decoration: none; cursor: pointer;
  transition: background-color .15s ease;
}
/* Reads as nothing until the island itself has gone white — the two
   hover states are meant to land together, not compete. */
.site-nav__link:hover { background-color: var(--neutral-100); }
.site-nav__link--trigger { display: inline-flex; align-items: center; gap: var(--space-1); }
.site-nav__chevron { transition: transform .15s ease; }
.site-nav__item[data-open] .site-nav__chevron { transform: rotate(180deg); }

.site-nav__panel {
  /* left:0 — was calc(--space-3 * -1) to hang the panel left of an
     unpadded trigger. The trigger now has --space-3 of inline padding
     of its own, so the panel aligns to its box edge instead.

     top is offset by --space-2 because the panel is positioned against
     the LINK, which sits 8px above the island's bottom edge (see the
     56px min-height above). At a plain top:100% the panel's 1px border
     and its rounded top corners get drawn across the middle of the white
     pill — a visible seam, confirmed under magnification. This lands the
     panel's top edge exactly on the island's bottom edge so it reads as
     hanging off the bar. */
  display: none; position: absolute; top: calc(100% + var(--space-2)); left: 0;
  min-width: 280px; background: #fff; box-shadow: var(--e3);
  border: 1px solid var(--neutral-200); border-radius: var(--radius-lg);
  padding: var(--space-4);
}
/* `block`, with the grid moved down onto .site-nav__panel-inner on
   2026-08-24. The panel itself has to stay a plain box here because below
   900px it becomes a one-track grid that animates 0fr -> 1fr, and that
   trick needs the wrapper as its single grid item — see the accordion note
   in the <900px block. Desktop renders identically either way: a block
   whose only child is the same grid that used to be the panel. */
.site-nav__item[data-open] .site-nav__panel { display: block; }
.site-nav__panel-inner { display: grid; gap: var(--space-2); }
.site-nav__panel a { display: block; padding: var(--space-2) var(--space-3); text-decoration: none; color: var(--ink-900); border-radius: var(--radius-sm); }
.site-nav__panel a:hover { background: var(--neutral-100); }
.site-nav__panel a span { display: block; font: var(--type-small); color: var(--neutral-600); }

.site-nav__panel--descriptive { width: 380px; }
.site-nav__panel--descriptive a { padding: var(--space-3); }
.site-nav__panel--descriptive strong { font: var(--type-h4); display: block; }
.site-nav__panel--descriptive span { margin-top: 2px; }

/* Hover bridge. The panel hangs --space-2 below the trigger (see the `top`
   note above), and that 8px strip belongs to neither box: a cursor crossing
   it fired mouseleave on the <li> and the panel closed before it could be
   reached. This pseudo-element fills the strip so the pointer never leaves
   the item on the way down. It is a child of the panel, so it counts as
   inside the <li> for mouseenter/mouseleave, and it exists only while the
   panel is displayed. It spans the panel's full width, not the trigger's,
   so a diagonal move toward a link on the panel's right stays covered — it
   sits below the 56px link band, so it never eats a sibling trigger's hover.
   Belt and braces with the close delay in main.js: the delay alone would
   still flicker the chevron, and the bridge alone would still lose a
   pointer that overshoots the panel's sides.
   Scoped >=900px because below that the panel is position: static inside
   the drawer accordion, where an absolutely positioned strip would land on
   the row above. */
@media (min-width: 900px) {
  .site-nav__panel::before {
    content: ""; position: absolute; left: 0; right: 0;
    top: calc(var(--space-2) * -1); height: var(--space-2);
  }
}

.site-header__utility { margin-left: auto; display: flex; align-items: center; gap: var(--space-4); }
/* 44px hit box. inline-flex keeps it in the utility row's flex line; the
   type and colour are unchanged, so this is a target fix, not a restyle.
   2026-08-24. */
.site-header__phone { font: var(--type-h4); color: var(--ink-900); text-decoration: none; white-space: nowrap; display: inline-flex; align-items: center; min-height: 44px; }
/* The drawer toggle. Hidden >=900px; the max-width:899px block below is
   what sizes and shows it.

   A HAMBURGER GLYPH SINCE 2026-08-21, replacing the literal word "Menu"
   (maintainer request). The word survives as .visually-hidden text in
   nav.njk so the button keeps an accessible name — see the note there.
   The reset is needed because this was a bare <button> relying on UA
   styling for its box; an icon button must not carry the native border
   and grey fill. */
.site-nav__toggle {
  display: none;
  background: none; border: 0; padding: 0;
  color: var(--ink-900); cursor: pointer;
}
/* The glyphs swap off aria-expanded, which main.js already maintains, so
   there is no second state class to keep in sync and the icon can never
   disagree with what the button reports to assistive tech.

   The hook is a wrapper span rather than a class on the icon itself: the
   "close" icon is shared with brand-dialog.njk, and restyling it here
   would reach into that dialog too.

   CROSS-FADED SINCE 2026-08-24, not display-swapped (maintainer: smooth
   out the mobile nav). `display` cannot be transitioned, so the old pair
   of rules popped one glyph out and the other in on the same frame while
   the drawer beneath it took .22s to arrive. Both glyphs now sit in the
   SAME grid cell — `grid-area: 1 / 1`, which is what the `inline-grid`
   in the <900px block is for — and swap on opacity plus a quarter turn.
   They counter-rotate (menu leaves at +90deg, close arrives from -90deg)
   so the two read as one continuous turn rather than two independent
   spins. Duration matches the drawer's .22s so the icon and the panel
   finish together.

   `visibility` rides along with opacity, and is not decorative: a
   0-opacity SVG still hit-tests, so without it the outgoing glyph would
   sit over the incoming one for the length of the transition. It is in
   the transition list for the same reason it is on .site-nav__drawer —
   going out it holds `visible` for the duration so the fade is seen,
   going in it flips immediately.

   The .visually-hidden "Menu" label is `position: absolute`, so it is
   taken out of grid flow and never claims a cell of its own.

   Reduced motion needs nothing here: base.css collapses every duration
   globally, which lands this back on an instant swap. */
.site-nav__toggle-icon {
  display: inline-flex; grid-area: 1 / 1;
  transition: opacity .22s ease, transform .22s ease, visibility .22s;
}
.site-nav__toggle-icon--close { opacity: 0; visibility: hidden; transform: rotate(-90deg); }
.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-icon--menu { opacity: 0; visibility: hidden; transform: rotate(90deg); }
.site-nav__toggle[aria-expanded="true"] .site-nav__toggle-icon--close { opacity: 1; visibility: visible; transform: none; }


/* --- Nav CTA: GoldKit's .gk-btn, scoped to the header ------------------
   Four layered effects, and they only read as "GoldKit" together: an inset
   bevel (top highlight + bottom shade), a 1px lift, a diagonal shimmer that
   sweeps across on hover, and an arrow badge that rotates 45deg to point
   up-and-right. Composes on top of base.css's .button — this modifier only
   adds surface, never layout.

   Scoped ON PURPOSE. base.css's .button--primary is flat gold everywhere
   else on the site (hero CTAs, cards, 404) — it was flat MARINE when this
   comment was written, and stopped being so on 2026-08-18; this is still
   the single place the bevel/shimmer treatment appears. Do not promote it
   to .button.

   The colour deliberately departs from the GoldKit spec. They set white on
   #dcaa31, which is ~2.3:1 — below WCAG AA. This site's own gold button
   pairs gold with ink instead (.button--on-dark), so this follows the kit:
   --gold-500 on --ink-900 is 6.4:1 at rest, and hover LIGHTENS to --gold-300
   (11.7:1) rather than darkening, which is the direction .button--on-dark
   already moves. Darkening to --gold-700 would have dropped ink to 3.5:1.

   prefers-reduced-motion is handled globally in base.css, which flattens
   every transition here to 0.01ms — the shimmer simply stops animating. Nothing extra needed. --- */
.button--nav {
  position: relative;
  overflow: hidden;              /* clips the shimmer to the button box */
  white-space: nowrap;           /* per the GoldKit spec — a two-line CTA
                                    inside a 56px pill breaks the stadium */
  background: var(--gold-500); color: var(--ink-900);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.32) inset,   /* top highlight */
    0 -2px 0 rgba(11, 14, 16, 0.12) inset,     /* bottom bevel */
    var(--e-gold-2),                           /* gold glow */
    0 2px 4px -2px rgba(11, 14, 16, 0.18);     /* contact shadow */
  transition:
    transform 140ms cubic-bezier(0.2, 0.8, 0.2, 1.4),  /* springy overshoot */
    box-shadow 160ms ease,
    background-color 160ms ease;
}
.button--nav:hover {
  background: var(--gold-300); color: var(--ink-900);
  transform: translateY(-1px);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.32) inset,
    0 -2px 0 rgba(11, 14, 16, 0.14) inset,
    var(--e-gold-3),                           /* glow expands */
    0 3px 6px -2px rgba(11, 14, 16, 0.2);
}
.button--nav:active { transform: translateY(1px); }

/* Shimmer: a diagonal light band parked off the left edge, swept across on
   hover. pointer-events:none so it never eats the click. */
.button--nav::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  background: linear-gradient(115deg,
    transparent 30%, rgba(255, 255, 255, 0.45) 48%, transparent 65%);
  transform: translateX(-120%);
  transition: transform 700ms ease;
  pointer-events: none;
}
.button--nav:hover::after { transform: translateX(120%); }

/* --- Ingot CTA: trapezoid gold button — DORMANT ------------------------
   NOTHING ON THE SITE USES THIS. Every rule below matches zero elements as
   the repo stands. It is kept, not abandoned: the maintainer asked on
   2026-08-19 to hold the styling for possible later use and to put the
   homepage CTA back as it was. Retained-but-off is this codebase's normal
   way of parking work — see js/stat-viz.js, pages.css §5.8, partials/
   quote.njk and the --marine-* tokens, all in the same state.

   TO SWITCH IT ON: add `button--ingot` alongside `button--gold` on any
   button. It was trialled on the "Learn More About Beyond79" CTA under Who
   We Are (src/index.njk), which is where to put it back to see it again.
   Nothing else is needed — no JS, no markup beyond the class.

   TWO DECISIONS WERE NEVER MADE, so do not read the current values as
   settled:
     * --ingot-slant is 10px. 6px was the recommendation, as gentler and more
       likely to survive site-wide use; it was never chosen.
     * --ingot-streak-a / --ingot-sweep-a are .28 / .45. Whether the static
       specular and the hover sweep are both wanted is open. The comparison
       that was supposed to settle it was invalid — the alphas were declared
       on ::before at the time, where a pseudo-element's own declaration beats
       the inherited value, so setting them on the element did nothing and all
       three "variants" rendered identically. They live on .button--ingot now
       and are genuinely overridable, so that comparison can simply be rerun.

   BEFORE ADOPTING IT SITE-WIDE, two things are known and neither is
   cosmetic:
     * It is NOT just adding a class. The background override below is scoped
       .button--gold.button--ingot. .button--on-dark and .button--primary
       paint their own gold rectangle over the z-index:-1 fill, so they need
       the override extended or the trapezoid never appears.
     * A sharp trapezoid sitting beside a 6px-rounded .button--secondary (the
       hero pairs exactly that) does not read as one family. Matching the
       outline buttons is a much larger change and should be decided first.

   It composes on top of base.css's .button and .button--gold and changes only
   the painted surface, never layout.

   THE SHAPE IS ON ::before, NOT ON THE BUTTON, and that is a correctness
   decision rather than a stylistic one. `clip-path` on the element itself
   would look identical and break two things:
     * clip-path clips HIT-TESTING as well as paint, so the sliced corners
       stop responding to clicks and the control drops below the kit's 44px
       minimum target at its edges;
     * it also clips the `:focus-visible` outline (base.css) out of
       existence, losing the keyboard focus indicator entirely.
   Painting the trapezoid on an absolutely-positioned pseudo-element keeps
   the element's own box rectangular, so the full target stays clickable and
   the focus ring still draws. The ring therefore stays a ROUNDED RECTANGLE
   around a trapezoid — a deliberate trade. Drawing a trapezoidal ring would
   mean `outline: none` plus another pseudo-element, which disappears in
   forced-colors mode; a slightly mismatched ring that always works beats a
   matching one that vanishes for the users who need it.

   Taper direction: both TOP corners pull inward, so the base is the wide
   edge. That is the draft angle a real cast bar has, and it sits visually
   grounded; inverting it reads as a keystone and looks top-heavy on a
   baseline.

   --ingot-slant is the single knob — the horizontal inset of each top
   corner. The inline padding grows by the same amount so the label never
   crowds the diagonals, which is why it must stay a custom property rather
   than a literal in the polygon.

   The background overrides are written as .button--gold.button--ingot
   (0,2,0 and 0,3,0) rather than relying on components.css loading after
   base.css. base.css's own .button--secondary comment records why: a
   specificity TIE broken by file order is exactly the fragility this
   codebase has already been bitten by. .button--gold:hover is (0,2,0), so a
   bare .button--ingot:hover would tie with it and the rectangle would paint
   gold-500 behind the trapezoid on hover.

   isolation: isolate is required. It gives the button its own stacking
   context so the z-index:-1 fill lands behind the label but still in front
   of whatever the button sits on; without it the fill can slide behind an
   ancestor's background and vanish. --- */
.button--ingot {
  --ingot-slant: 10px;

  /* These live on the ELEMENT, not on ::before, and that placement is
     load-bearing. A pseudo-element's own declaration beats a value inherited
     from its originating element, so declaring them inside ::before made them
     impossible to override from the outside — setting --ingot-streak-a on the
     button had no effect whatsoever, silently. Declared here they inherit into
     ::before and stay overridable per-instance. */
  --ingot-streak-a: .28;      /* static specular, 0 disables */
  --ingot-sweep-a: .45;       /* hover sweep, 0 disables — matches .button--nav */
  /* --ingot-edge exists because the lit top face cannot also be the button's
     top EDGE. --gold-100 (#F6ECD6) against this site's page background
     (#FAF9F7) measures 1.12:1 — the top of the bar simply dissolved into the
     page, and the shape only looked like it had a top when hover happened to
     darken it. So the gradient opens on a DARK rim and the --gold-100
     highlight sits just BELOW it — which is also where light actually falls on
     a cast bar: on the top face, not on the extreme edge.

     --gold-300 was tried here first and was not enough: measured down the
     rendered top edge it reached only 1.32:1, because ::before also carried a
     1px white inset bevel that washed out the very rows the rim was meant to
     define. The white top bevel is gone for that reason — do not reinstate it,
     it defeats this. --gold-500 gives ~2.9:1 against the page. The bottom
     bevel stays; it has no such conflict. The paired 1px hairline in `filter`
     finishes the two diagonals, where no gradient stop can help. */
  --ingot-edge: var(--gold-500);
  --ingot-top: var(--gold-100);
  --ingot-mid: var(--gold-300);
  --ingot-base: var(--gold-500);

  position: relative;
  isolation: isolate;
  padding-inline: calc(var(--space-6) + var(--ingot-slant));
}
.button--gold.button--ingot,
.button--gold.button--ingot:hover { background: transparent; }
.button--ingot::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;

  /* --- The lit face -------------------------------------------------
     Three stacked background layers, top of the list painting first:
       1. the hover sweep — a light band parked off the LEFT edge that
          travels across the face on hover;
       2. a fixed specular streak, the highlight a polished bar carries
          at rest;
       3. the cast-bar face itself, light at the top edge and deepening
          toward the base, which is what makes it read as a lit solid
          rather than a flat gold shape.

     The two white bands are alpha custom properties so the amount of
     shine is one number each, and setting either to 0 removes that layer
     without deleting anything.

     CONTRAST FLOOR: --ingot-base never goes below --gold-500, at rest or on
     hover. The label is ink-900; measured against the rendered face the
     background under the text band is ~8.9:1 at rest, and the darkest point
     anywhere on the bar is the 2px bottom bevel at ~5.2:1, which no text
     overlaps. gold-700 would take the base to 3.5:1 and fail. Do not deepen
     the base past gold-500 to make the bar look heavier.

     HOVER DOES NOT TOUCH THE TOP (maintainer, 2026-08-19: the lit top must
     read the same before and after hover). Only --ingot-mid deepens and the
     sweep travels; --ingot-edge and --ingot-top are deliberately absent from
     the :hover rule. */
  background-color: var(--ingot-mid);
  background-image:
    linear-gradient(115deg, transparent 0%, rgba(255, 255, 255, var(--ingot-sweep-a)) 50%, transparent 100%),
    linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, var(--ingot-streak-a)) 50%, transparent 62%),
    linear-gradient(to bottom,
      var(--ingot-edge) 0%,     /* see the --ingot-edge note on .button--ingot */
      var(--ingot-top) 18%,     /* the lit top face */
      var(--ingot-mid) 54%,
      var(--ingot-base) 100%);
  /* The sweep layer is sized to half the button so its percentage positions
     have somewhere to travel: with background-size 100% the offset resolves
     against (container - image) = 0 and percentages become no-ops. At 50%
     wide, -120% parks it fully clear of the left edge and 220% clears the
     right. It cannot use .button--nav's transform trick — that rides on a
     pseudo-element, and ::after is already the focus ring. */
  background-size: 50% 100%, 100% 100%, 100% 100%;
  background-position: -120% 0, 0 0, 0 0;
  background-repeat: no-repeat;

  /* Inset bevel, same idea and nearly the same values as .button--nav: a
     1px lit top edge and a soft shade along the base. Inset shadows are
     clipped by clip-path along with everything else, so these follow the
     trapezoid's flat top and bottom; the two diagonals are deliberately
     left clean, which is also where a real bar's chamfer catches least. */
  box-shadow: 0 -2px 0 rgba(11, 14, 16, 0.14) inset;

  /* Contact shadow via filter, NOT box-shadow: an outer box-shadow is clipped
     away entirely by clip-path, while drop-shadow follows the clipped alpha
     and so takes the trapezoid's outline. It cannot reuse --e2 verbatim
     because drop-shadow has no spread parameter; this is --e2's colour and
     softness with the spread folded into the blur. Neutral on purpose —
     tokens.css fences --e-gold-* to the nav pill and its CTA. */
  filter: drop-shadow(0 3px 6px rgba(11, 14, 16, 0.16))
          drop-shadow(0 0 1px rgba(11, 14, 16, 0.28));

  /* Crisp vertices, no radius: a cast bar has hard edges, and the polygon
     would cut any rounding off at the two slanted corners anyway. */
  clip-path: polygon(var(--ingot-slant) 0,
                     calc(100% - var(--ingot-slant)) 0,
                     100% 100%,
                     0 100%);
  transition: background-position 700ms ease, background-color .15s ease;
}
/* Hover: the whole face steps one stop deeper and the sweep crosses. The base
   stop stays at gold-500 rather than following the others down, holding the
   6.4:1 contrast floor described above. base.css flattens transitions to
   0.01ms under prefers-reduced-motion, so the sweep simply does not travel —
   the same way .button--nav's shimmer behaves, and nothing extra is needed. */
.button--ingot:hover {
  --ingot-mid: color-mix(in srgb, var(--gold-500) 45%, var(--gold-300));
}
.button--ingot:hover::before { background-position: 220% 0, 0 0, 0 0; }
/* Focus ring, shaped to match.
   The default `outline` (2px gold-700 at 2px offset, tokens.css) draws round
   the element's RECTANGULAR box, which leaves two obvious wedges of page
   showing at the tapered top corners — it reads as a rendering fault rather
   than as a focus indicator. So the ring is redrawn as a trapezoid.

   One pseudo-element, one polygon: listing the outer contour and then the
   inner one in a single `polygon()` with the `evenodd` fill rule punches the
   middle out, leaving a genuine 2px band with a 2px gap inside it — the same
   geometry the token describes, following the shape instead of a rectangle.

   The two slants are scaled so all three edges stay PARALLEL. A diagonal's
   angle is its horizontal run over its height, so a contour drawn on a taller
   box needs a proportionally longer run: the ::after box is 8px taller than
   the 44px button (inset -4px), hence 52/44, and the inner contour sits 2px
   further in, hence 48/44. CSS cannot divide a length by a length, so these
   are unitless ratios rather than a calc over --ingot-h. They assume .button's
   44px min-height and a single-line label, which is every CTA on this site;
   if one ever wrapped to two lines the ring's angle would be off by about a
   degree, i.e. under a pixel. Not worth parameterising.

   forced-colors restores the real outline. Clip-path shapes and backgrounds
   are discarded in that mode, so the trapezoid ring would simply vanish —
   the users who most need a focus indicator would lose it. */
.button--ingot:focus-visible { outline: none; }
.button--ingot:focus-visible::after {
  content: "";
  position: absolute;
  inset: -4px;
  z-index: -2;
  pointer-events: none;
  background: var(--gold-700);
  clip-path: polygon(evenodd,
    /* outer edge of the ring */
    calc(var(--ingot-slant) * 52 / 44) 0,
    calc(100% - var(--ingot-slant) * 52 / 44) 0,
    100% 100%,
    0 100%,
    /* inner edge — punched out by evenodd */
    calc(2px + var(--ingot-slant) * 48 / 44) 2px,
    calc(100% - 2px - var(--ingot-slant) * 48 / 44) 2px,
    calc(100% - 2px) calc(100% - 2px),
    2px calc(100% - 2px));
}
@media (forced-colors: active) {
  .button--ingot:focus-visible { outline: var(--focus-ring); outline-offset: 2px; }
  .button--ingot:focus-visible::after { display: none; }
}

/* --- The 900-1199px squeeze -------------------------------------------
   Everything in the pill sits on one nowrap line, so between the 900px
   drawer breakpoint and ~1200px the row has to be actively made to fit
   rather than allowed to wrap. --space-3 of padding on each of the four
   links (+96px, only partly repaid by the tighter list gap) landed with
   the GoldKit treatment on 2026-08-17 and overran the row at 900px,
   wrapping both the phone number and the CTA onto second lines. So:
   tighten the inter-zone gaps and the link padding. The links keep their
   pills. (The CTA's 22px arrow badge, dropped here first because it was
   decorative, is gone from the design entirely as of 2026-08-18.) --- */
@media (min-width: 900px) and (max-width: 63.99em) {
  .site-header__inner { gap: var(--space-4); }
  .site-nav__link { padding-inline: var(--space-2); }
  .site-header__utility { gap: var(--space-3); }
}

@media (max-width: 899px) {
  /* Content-height drop-down: two accordions (About, Brands) plus two
     flat links on 56px divided rows, then the phone + Contact us row as
     the drawer's own last row. That button is the only Contact route in
     the header — see nav.njk. (Kit §4.1 describes a full-height sheet
     with 48px rows and a viewport-pinned CTA bar; all three were
     retired by the maintainer, on 2026-08-21 and 2026-08-24.) */
  /* Undo the floating island: below 900px this is the solid full-bleed
     bar it always was. The drawer below is `position: fixed; inset:
     72px 0 0`, measured off this bar — a translucent, inset, 56px
     island would leave it hanging in mid-air. */
  /* min-height back to 72px: the desktop band is 88px, but the drawer is
     pinned at `inset: 72px 0 0` and the mobile tabbar/:target numbers are
     derived from 72+1. Mobile geometry is deliberately unchanged. */
  .site-header {
    min-height: 72px;
    background: #fff; border-bottom: 1px solid var(--neutral-200);
  }
  /* Undoes the pill: below 900px the white surface, hairline and shadow all
     belong to .site-header above, which is full-bleed, so the inner element
     must contribute nothing. The :hover/:focus-within/:has reset that used
     to sit here went with the desktop hover state it was countering. */
  .site-header__inner {
    min-height: 72px; margin-block: 0;
    border: 0; border-radius: 0; box-shadow: none;
    background-color: transparent;
  }

  /* `flex: 0 0 auto` IS WHAT PUTS THE TOGGLE AT THE RIGHT EDGE, and it is
     what makes the `margin-left: auto` beside it do anything at all.

     .site-nav carries `flex: 1` for the desktop bar, where it has to span
     from the logo to the right edge so .site-header__utility's own
     `margin-left: auto` has room to push against. Below 900px
     .site-nav__drawer is `position: fixed` and therefore out of flow, so
     the toggle is the only in-flow child left — and with `flex: 1` still
     inherited from that desktop rule, .site-nav grew to fill everything
     after the logo and the toggle sat at its FLEX-START.

     Measured at 390px before the fix: .site-nav spanned x=206..366 and the
     toggle landed at x=206, i.e. 116px short of the right gutter, which is
     what read as "closer to the center". `margin-left: auto` could not fix
     that on its own and was inert: flex-grow distributes positive free
     space BEFORE auto margins are resolved, so by the time the margin was
     considered there was none left to absorb. Shrinking the basis to the
     toggle's own width hands that space back to the margin.

     Fixed 2026-08-24 at the maintainer's request. Do not restore `flex: 1`
     here on the theory that it matches the desktop rule — it is exactly
     the bug. */
  .site-nav { flex: 0 0 auto; margin-left: auto; }
  /* min-width matches min-height so the glyph keeps the full 44px touch
     target the word "Menu" used to give it by being wide.
     `inline-grid`, not `inline-flex`, since 2026-08-24: the two glyphs are
     stacked in one cell so they can cross-fade instead of display-swapping
     — see .site-nav__toggle-icon above. `place-items: center` replaces the
     align/justify pair and centres the shared cell in the 44px box. */
  .site-nav__toggle {
    display: inline-grid; place-items: center;
    min-height: 44px; min-width: 44px;
    /* The touch target is 44px but the glyph is 18px, so ~13px of the
       button's box hangs past the gutter on the right. Pulling it back by
       half that difference optically aligns the bars of the hamburger with
       the container edge the logo starts from, instead of leaving the row
       looking short on the right. */
    margin-right: calc((44px - 18px) / -2);
  }
  /* CONTENT-HEIGHT DROP-DOWN, not a full-viewport sheet (maintainer,
     2026-08-21: "I don't want the mobile nav to take up the entire screen
     upon opening"). It hangs off the bottom of the 72px bar and is exactly
     as tall as the links plus the utility row, so the page stays visible
     underneath.

     `bottom` is deliberately NOT set — `top: 72px` with `left/right: 0`
     and no bottom lets the height come from the content. The max-height is
     the safety net for a long menu or a small landscape phone, and it is
     `dvh` rather than `vh` on purpose: `vh` on mobile is the LARGEST
     viewport (browser chrome retracted), so a `100vh - 72px` panel would
     run underneath the address bar and hide its own last row. `dvh`
     tracks the chrome.

     Revealed by animating visibility/opacity/transform rather than being
     switched on with `display`, which cannot be transitioned.
     `visibility` IS LOAD-BEARING and does both jobs `display: none` used
     to do: it keeps the closed panel out of the tab order, and it stops it
     swallowing pointer events. Do not replace it with opacity alone.

     It is in the transition list deliberately. Going closed, visibility
     holds `visible` for the duration so the fade is actually seen; going
     open, it flips immediately. Reduced motion needs nothing here — the
     global rule in base.css already collapses these durations. */
  .site-nav__drawer {
    display: block; position: fixed; top: 72px; left: 0; right: 0;
    max-height: calc(100dvh - 72px); overflow-y: auto;
    background: #fff; border-bottom: 1px solid var(--neutral-200);
    box-shadow: var(--e3);
    visibility: hidden; opacity: 0; transform: translateY(-6px);
    transition: opacity .22s ease, transform .22s ease, visibility .22s;
  }
  .site-nav[data-open] .site-nav__drawer {
    visibility: visible; opacity: 1; transform: none;
  }
  /* The list is a plain block inside the panel now — the fixed positioning
     and the 96px bottom padding it used to carry both belonged to the
     full-screen sheet and to clearing the viewport-pinned utility bar,
     neither of which exists any more.

     ROWS ARE FLUSH AND DIVIDED SINCE 2026-08-24, replacing a --space-6
     gap between free-floating rows (maintainer). Four 48px rows separated
     by 24px of nothing read as four unrelated labels rather than one menu,
     and — with the accordions now animating — a gap would have opened a
     24px hole above every sub-panel as it slid out. Taller rows (56px, up
     from 48px) plus a hairline between them buy back the separation the
     gap was providing and cost 40px less height overall. */
  .site-nav__list {
    display: flex; flex-direction: column; gap: 0;
    padding: var(--space-2) var(--gutter);
  }
  /* On the item, not the link: the sub-panel is inside the item, so this
     draws the rule above each top-level row and never between a trigger
     and its own open panel. Nothing above the first row — the drawer's
     own top edge already sits against the header's bottom border. */
  .site-nav__item + .site-nav__item { border-top: 1px solid var(--neutral-200); }

  /* THE ACCORDION SLIDE. The sub-panels used to switch display: none ->
     block, which is unanimatable, so About/Brands snapped open inside a
     drawer that had just faded in over .22s.

     `grid-template-rows: 0fr -> 1fr` is the one way to transition to an
     unknown content height without measuring it in JS: the fr unit is
     interpolable where `height: auto` is not. In an auto-height grid
     container a track of `Nfr` (N <= 1) resolves to N x the content
     height, so the panel sweeps linearly from 0 to its natural size.

     THE GRID IS ON THE PANEL AND HAS EXACTLY ONE TRACK AND ONE ITEM.
     Both halves of that are load-bearing, and the first draft got it
     wrong: it put `grid-template-rows: auto 0fr -> auto 1fr` on the <li>,
     with the trigger in row 1 and the panel in row 2. THAT VISIBLY
     BROKE — the `auto` track resolves against a container height that the
     fr track is changing at the same time, so the two tracks fought over
     the slack and the 56px trigger row inflated to 122px mid-animation
     and deflated again, shoving the panel down and dragging it back up.
     Traced track-by-track over CDP: 56 -> 68 -> 88 -> 116 -> 122 -> 106
     -> 91 -> 78 -> 62 -> 56. Never put an `auto` track in a
     grid-template-rows transition; keep the trigger outside the animating
     grid entirely, which is what .site-nav__panel-inner is for.

     THREE MORE THINGS ARE REQUIRED and none is sufficient alone:
       * `overflow: hidden` on the inner wrapper, or the "collapsed"
         content spills out of its 0-height track over the row below;
       * `min-height: 0` on it too, because a grid item's automatic
         minimum size is its content and would refuse to shrink past it;
       * NO PADDING ON THE PANEL ITSELF. Padding sits outside the content
         box the track sizes, so a panel with the old
         `padding: 0 0 --space-2 --space-4` stayed 8px tall when closed —
         a visible sliver under every shut accordion. The indent and the
         trailing space live on the anchors, where the wrapper's own
         overflow clips them. */
  .site-nav__panel-inner {
    overflow: hidden; min-height: 0;
    /* Undo the desktop grid + --space-2 gap: the drawer rows space
       themselves with their own 48px min-height and padding, and looked
       right that way before this wrapper existed. */
    display: block;
  }

  .site-nav__link {
    min-height: 56px; width: 100%; justify-content: space-between;
    padding-inline: 0; border-radius: var(--radius-sm);
  }
  /* :active, not :hover — a touch pointer has no hover, and the desktop
     `.site-nav__link:hover` pill would otherwise stick to whichever row
     was tapped last until something else was touched. This is the tap
     feedback the drawer had none of. */
  .site-nav__link:active { background-color: var(--neutral-100); }

  /* `display: grid` here rather than none: below 900px the panel is
     always rendered and the 0fr track is what collapses it. `visibility`
     is what keeps a collapsed panel out of the tab order and off the
     hit-test — zero height alone does neither — and it transitions on the
     same .26s as the track so it holds `visible` through the closing
     slide and flips straight to visible on the way open. */
  .site-nav__panel {
    position: static; box-shadow: none; border: 0;
    padding: 0; min-width: 0; width: auto;
    display: grid; grid-template-rows: 0fr;
    visibility: hidden;
    transition: grid-template-rows .26s ease, visibility .26s;
  }
  /* Same specificity as the desktop `[data-open] ... { display: block }`
     rule and later in the file, which is what beats it. */
  .site-nav__item[data-open] .site-nav__panel { display: grid; grid-template-rows: 1fr; visibility: visible; }
  .site-nav__panel a {
    min-height: 48px; display: flex; flex-direction: column; justify-content: center;
    padding: var(--space-2) 0 var(--space-2) var(--space-4);
  }
  .site-nav__panel a:active { background-color: var(--neutral-100); }
  /* The panel's trailing gap, as anchor padding rather than panel padding
     — see the accordion note above for why it cannot live on the panel. */
  .site-nav__panel a:last-child { padding-bottom: var(--space-4); }
  /* Static, not `position: fixed; bottom: 0`. It is the last row of the
     open panel now rather than a bar pinned to the bottom of the viewport
     — see nav.njk, where it also moved in the DOM. The z-index and the
     white background went with the pinning: it sits on the panel's own
     white surface and no longer has to paint over page content.

     The `.site-nav:not([data-open]) ~ .site-header__utility { display:
     none }` rule that used to follow is DELETED, not misplaced. It existed
     to hide a viewport-pinned bar while the drawer was shut; the utility
     is inside .site-nav__drawer now, so it is hidden and shown by the
     panel's own visibility. The `~` combinator it used would no longer
     match in any case, since the two are no longer siblings. */
  /* `justify-content: space-between` added 2026-08-24: the row inherited
     the desktop utility's plain `display: flex` with no distribution, so
     both children packed to the left and the CTA came to rest wherever the
     phone number's width happened to leave it — x=153 of 390 at the time,
     i.e. floating in the middle of the row against 84px of empty space on
     its right. Splitting the row pins the phone to the left gutter and the
     button to the right one, so the CTA lines up with the toggle above it
     and the row has a deliberate shape at any phone width. `gap` stays as
     the minimum separation for the narrowest screens, where
     space-between's free space runs out. */
  .site-header__utility {
    display: flex; position: static; margin-left: 0;
    justify-content: space-between;
    border-top: 1px solid var(--neutral-200);
    padding: var(--space-3) var(--gutter); gap: var(--space-4);
  }
}

/* ============================================================
   4.3 — Brand feature panels. Half-width panels, side-by-side
   ≥900px, stacked below. Equal height via grid (grid-template-rows
   on the panel itself), not fixed heights — the CTA row always
   pins to the bottom regardless of how long the paragraph runs.
   Caller wraps N `.brand-panel` includes in `.brand-panels`.
   ============================================================ */
/* `align-items: stretch` equalises the two cards SIDE BY SIDE; it does
   nothing once they stack, because then each card is its own grid row and
   rows size to their own content. `grid-auto-rows: 1fr` is what equalises
   them in that stacked case — with a content-height container every row
   resolves to the tallest, so the cards stay identical at every width.

   It is not hypothetical. At 480px the two cards drifted 9px apart, from
   two unrelated content differences that partly cancelled: Recycle
   Platinum's logo renders 219px wide against Sell Your Gold's 189px, so its
   head row wrapped the eyebrow under the logo (+27px), while Sell Your
   Gold's longer tagline wrapped to two lines (+18px). Both are properties
   of the content, not bugs to chase individually — equal rows absorb any
   such difference. */
/* THE CARD WASH — a gold radial bloom in the panel's bottom-right corner,
   added 2026-08-20 (maintainer: the Our Brands cards "need a bit more work
   utilizing the space fully"). The closed card carries only four things
   (head, tagline, Explore, and the display:none dialog) by the 2026-08-18
   decision, so the lower-right quadrant of a 519x229 tile was empty
   surface. This fills it with figure-ground rather than with copy — NOTHING
   was added to read, which is the constraint that decision imposes.

   `--brand-wash` is REGISTERED rather than a plain custom property, and that
   is the whole point: an unregistered custom property has no type, so it
   cannot be interpolated and the hover step would swap the gradient in one
   frame. Registered as a <percentage> it animates, so the bloom strengthens
   with the rest of the hover instead of snapping. `.brand-panel` never
   declares the rest value — 9% is this rule's initial-value — so there is
   exactly one place to retune the resting strength.

   Where @property is unsupported the declaration is ignored, `--brand-wash`
   falls back to an untyped custom property, and everything still renders:
   the wash is present at both strengths and only the interpolation is lost.

   ONE WASH FOR BOTH BRANDS, deliberately. Tinting each card to its own logo
   would put Recycle Platinum's blue back on the homepage, which §4 of
   CLAUDE.md rules out — marine is dormant on this site. Gold is the shared
   Beyond79 system colour and is what the stat band and every hairline use.
   gold-500 is legal here because this is a decorative surface, not text:
   the "large/non-text only" note in tokens.css is a contrast rule about
   glyphs, and nothing is set on top of the bloom's dense end. */
@property --brand-wash {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 9%;
}
.brand-panels { display: grid; gap: var(--space-8); align-items: stretch; grid-auto-rows: 1fr; }
/* 70em (1120px), RAISED FROM 900px on 2026-08-18, and it is derived from the
   one-line tagline requirement rather than picked as a device width.

   Two abreast, a panel's content box is (vw - 48 gutter - 32 gap) / 2 - 64
   padding. The longest tagline sets 26.43x its font-size, so holding it on
   one line at roughly the 17px body-copy size needs ~449px of panel, which
   first happens near 1106px of viewport. Measured at this breakpoint the
   tagline computes 16.5px — a shade under body copy, because the 3.7cqw
   coefficient carries a safety margin under the exact 3.784 fit — and it
   reaches 18px once the container maxes out at 1200px. Below 70em the
   panels stack, where they are wide enough for the full 21px --type-h3.

   At the old 900px the 900-1023px band was 2-up with only 398px of panel:
   the tagline floored at 14px and wrapped to two lines anyway, SMALLER than
   the 17px body copy beneath it. Lowering this breakpoint again brings that
   back. If it must go lower, the tagline copy has to get shorter first. */
/* 70em -> 64em normalisation TRIED and REVERTED, 2026-08-24: mobile-audit
   sweep at 640/900/1024/1120/1200/1280px showed a -320px height delta on
   `/` at 1024px (the homepage's own brand panels flip 1-up -> 2-up a
   breakpoint early), zero delta everywhere else. That is exactly the
   398px-panel/tagline-wrap regression the comment above already warns
   about for the "old 900px" band, just arriving at 1024 instead. This is
   a real breakpoint, not a stray value — leave it at 70em. */
@media (min-width: 70em) { .brand-panels { grid-template-columns: 1fr 1fr; } }
.brand-panel {
  /* THREE tracks for three children: head (logo + role eyebrow on one row),
     tagline, and the affordance — which takes the 1fr and pins itself to the
     floor with its own `align-self: end`, so cards of unequal tagline height
     still line their "Explore" rows up.

     This has been shrinking all through 2026-08-18: six tracks, then five
     when the chip row came off, then four when the logo and eyebrow merged
     into .__head, then three when the body paragraph moved into the dialog.
     Add a track back for each thing restored — a chip <ul>, a body
     paragraph, or a head that gets split apart again. */
  display: grid; grid-template-rows: auto auto 1fr;
  /* The gradient lives in its own custom property because the card's fill is
     set TWICE — white here, neutral-100 when the card sits inside
     .brands__shell (pages.css §5.3) — and both use the `background`
     SHORTHAND, which resets background-image to `none`. A wash declared as a
     bare `background-image` on this rule would therefore be wiped out by
     §5.3 and appear only where the panel is reused outside the shell, which
     is nowhere. Holding it in a variable lets both declarations name it.
     Keep it that way: if you add a third fill, pass `var(--brand-wash-image)`
     with it. */
  --brand-wash-image: radial-gradient(125% 105% at 100% 100%,
      color-mix(in srgb, var(--gold-500) var(--brand-wash), transparent) 0%,
      transparent 62%);
  background: #fff var(--brand-wash-image) no-repeat;
  border-radius: var(--radius-lg); box-shadow: var(--e1);
  border: 1px solid var(--neutral-200); padding: var(--space-8);
  /* Query container for .brand-panel__tagline's fluid size — see the long
     note on that rule. `inline-size` only, so the panel's HEIGHT still grows
     with its content and .brand-panels' `align-items: stretch` keeps the
     two cards level. Do not promote this to `container-type: size`, which
     would contain the block axis and collapse the card. */
  container-type: inline-size;
  /* The whole card is an <a> since 2026-08-18 (see brand-panel.njk). These
     four lines are the link reset: without them the card inherits the
     marine link colour and an underline across every child, tagline and
     body copy included. `color: inherit` puts the tagline back to ink and
     lets .brand-panel__body keep its own neutral-600. */
  color: inherit; text-decoration: none;
  /* `--brand-wash` is in the transition list and background-color is not, on
     purpose. The rest->hover fill step (neutral-100 -> white, §5.3) has been
     instant since the shell landed and stays instant; interpolating it would
     drag the card through a range of off-whites that reads as a lag on a
     500px surface. The wash is the part that gains from easing. */
  transition: box-shadow .18s ease, border-color .18s ease, transform .18s ease,
              --brand-wash .18s ease;
}
/* MINIMUM CARD HEIGHT, 2026-08-20. Content-height alone left these at 229px
   against 519px of width at desktop — a 2.27:1 letterbox — and the tablet
   band is worse, because between 40em and 70em the panels are full-width
   single-column at the same 229px. 18rem (288px) is the smallest step that
   reads as a bento tile rather than a strip.

   SCOPED TO >=40em BECAUSE PHONES DO NOT HAVE THIS PROBLEM. At 390px the
   card interior is ~214px wide, the head row wraps the eyebrow under the
   logo and the tagline runs to two or three lines, so the card is already
   past 288px on its own content. Applying a floor there would be inert at
   best and, if the copy ever shortened, would add height to the one
   viewport that can least afford it.

   The extra height lands in the third grid track — the `1fr` holding
   .brand-panel__more — so the Explore row simply pins lower and the
   identity block at the top does not move. Both cards share one grid row,
   so the whole change costs the page ~59px of scroll, not 118px. That is
   worth knowing against the 2026-08-19 shell-padding cut, which was made to
   reduce exactly this: the two decisions pull opposite ways and this one is
   the later of them. */
@media (min-width: 40em) { .brand-panel { min-height: 18rem; } }
/* Hover/focus lift. --e2 is the token's designated hover elevation and the
   gold hairline matches the stat band's hover treatment (§4.5), so the two
   interactive surfaces on the homepage agree with each other.

   The translate was 1px until 2026-08-20 and is 2px now, moved WITH the
   18rem minimum height above and not independently of it. The old comment
   here read "deliberately tiny: these are 500px-wide cards and anything
   larger reads as a jump rather than a lift" — that judgement was made
   against a 229px-tall strip, where 2px was a visible hop. On a 288px tile
   the same 2px is proportionally the lift 1px used to be. If the minimum
   height is ever taken back out, take this back to 1px with it.

   `--brand-wash` nearly doubles here, 9% -> 16%. It is the only part of the
   hover that eases (see the transition list above); the fill, shadow,
   hairline and translate all behave as they did. base.css flattens every
   transition under prefers-reduced-motion, this one included. */
.brand-panel:hover,
.brand-panel:focus-visible {
  --brand-wash: 16%;
  box-shadow: var(--e2); border-color: var(--gold-500); transform: translateY(-2px);
}
/* The expand affordance, sitting where the "Learn More About ..." button
   used to. Not a button and not a link — the card is the control — so it
   is styled as a label, not as a CTA: at the same weight/size as .button's
   label so the row it occupies keeps its old visual mass. `align-self: end`
   pins it to the floor of the card the way .brand-panel__cta did.

   gold-700, matching the site's link colour (maintainer, 2026-08-18 — this
   shipped marine-500 earlier the same day and was the first thing called
   out). It is the only gold legal as text on this white card at 5.52:1;
   gold-500 would be 2.89:1 and is for rules and icons, not labels. Hover
   darkens to ink because there is no legal gold below gold-700 to move to,
   and the card's own gold-500 hairline carries the gold on hover instead. */
.brand-panel__more {
  align-self: end; display: inline-flex; align-items: center; gap: var(--space-2);
  font: var(--type-h4); color: var(--gold-700);
}
.brand-panel:hover .brand-panel__more { color: var(--ink-900); }
/* The plus rotates into a "more" gesture on hover. Purely decorative and
   flattened under prefers-reduced-motion by base.css's global rule. */
.brand-panel__more .icon { transition: transform .18s ease; }
.brand-panel:hover .brand-panel__more .icon { transform: rotate(90deg); }
/* Brand logo, homepage panel (white surface). Height is fixed and width
   left auto because the two supplied logos have different aspect ratios
   (SYG 460x175, RP 460x151) — sizing both to one width would render them at
   visibly different heights and break the row. Height stays the fixed
   dimension for the same reason.

   This comment described a `justify-self: center` and a centred tagline
   until 2026-08-18. Neither is here any more: the logo became a flex item
   of .brand-panel__head that day (so `justify-self` would have been ignored
   there regardless), and the maintainer left-aligned the tagline later the
   same day. The logo now sits hard left against the panel's content-box
   edge, which is what the tagline aligns to.

   The same passage also documented a `margin-bottom` on this rule that
   floored the gap under logos with differing internal whitespace. That
   declaration is gone too, and the measurement behind it no longer holds:
   the supplied PNGs carry almost no transparent padding — SYG's ink box is
   453x169 of 460x175 (about 2.5px of slack at 72px tall) and RP's is the
   full 460x151 (none). The gap under the head row is now set once, on
   .brand-panel__head below, and applies to both. Do not re-add a per-logo
   margin to compensate for whitespace that is not there.

   OPTICAL SIZE IS NOT EQUAL BETWEEN THESE TWO, AND CSS CANNOT FIX IT.
   The files are structurally different lockups: SYG is wordmark-only, one
   continuous ink band over its full height, so at 72px its wordmark reads
   ~69px tall. RP is a stacked lockup — a centred icon (source rows 0-86)
   above its wordmark (rows 119-150) — so at the same 72px box its wordmark
   reads only ~15px, roughly a QUARTER of SYG's. Matching them by wordmark
   would need RP's box near 190px tall, which busts both the layout and the
   retina budget below. This needs an asset, not a rule: a horizontal or
   wordmark-only RP lockup, or the SVGs these slots originally waited on. */
/* The head row: logo hard left, role eyebrow hard right, vertically centred
   against each other (maintainer, 2026-08-18 — "logo on the left of the box
   and flagship brand on the right").

   `flex-wrap: wrap` is not tidiness. On phones the panel interior is ~278px
   (390px viewport), while the logo alone renders up to 219px and the eyebrow
   needs ~130px — they cannot share a line there. Wrapping drops the eyebrow
   under the logo instead of overflowing the card. (This said "<900px" until
   2026-08-18; the stacking breakpoint is 70em now, but full-width panels at
   tablet size are wide enough for one row — it is only phones that wrap.) */
/* The card's vertical rhythm is set in exactly two places — this
   margin-bottom and .brand-panel__tagline's — and both are on the token
   scale on purpose. Top to bottom the card now reads 32 / 16 / 32:
   --space-8 of padding, --space-4 under the head, --space-8 under the
   tagline, --space-8 of padding again. The 1:2 step keeps the logo and
   tagline reading as one identity block with the "Explore" action set
   apart from it.

   It was --space-2 (8px) here until 2026-08-18, against 16px under the
   tagline — an 8/16/32 sequence that was neither even nor a clean ratio,
   and 8px was very tight under a 72px logo. Change these two values
   together or the rhythm goes lopsided again. */
.brand-panel__head {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--space-4);
  margin-bottom: var(--space-4);
}
.brand-panel__logo { height: 72px; width: auto; }
/* 72px, and NOT larger, is a resolution ceiling rather than a taste call
   (maintainer asked to enlarge these, 2026-08-18; they were 44px).

   Both PNGs are 460px wide intrinsically. To stay sharp on a 2x display the
   rendered CSS width must not exceed 460/2 = 230px. Recycle Platinum is the
   wider ratio at 3.046, so it binds: 230 / 3.046 = 75.5px of height. Sell
   Your Gold (2.629) would tolerate 87px. 72px keeps both inside the budget
   with a little slack — at 72px they render 189px and 219px wide, needing
   378px and 438px of source against the 460px available.

   Going past ~75px starts softening Recycle Platinum on retina. If bigger
   is wanted, the logos need re-exporting at a higher resolution first, or
   replacing with the SVGs these slots originally waited on. */
/* ONE LINE (maintainer, 2026-08-18). The size is fluid because a fixed one
   cannot do it: at --type-h3's 21px the longer tagline ("One of the Nation's
   Leading Online Precious Metals Buyers") sets 555px wide, and a 2-up panel
   never offers more than 486px — the container caps at 1200px, so no
   viewport exists where 21px fits two abreast. Measured, not estimated.

   So the size is tied to the PANEL's own width via container query units
   (`container-type: inline-size` on .brand-panel above), which is what makes
   this work at 2-up and 1-up without a single breakpoint: 1cqw is 1% of the
   panel's content box, and the tagline's width is 26.43x its font-size, so
   `avail / 26.43` = 3.784cqw is the exact fit. 3.7cqw is that with a margin
   for font-metric rounding. Re-derive the coefficient if the copy changes —
   it is a property of the longest tagline, not a magic number.

   The 21px ceiling keeps it at --type-h3 once the panel is wide enough (the
   1-up layout below 900px), so nothing gets BIGGER than the kit's heading.
   The 14px floor is tokens.css's "NEVER below 14px" rule, and it binds on
   phones: at 390px the panel offers 276px and one line would need ~10px, so
   the tagline wraps to two lines there instead of becoming unreadable. That
   is the one place this does not deliver one line, and it is deliberate.

   NO `white-space: nowrap`. It is not needed — a correctly sized line does
   not wrap — and adding it would turn the floored phone case into
   horizontal page overflow instead of a second line. */
/* LEFT-ALIGNED, flush with the logo (maintainer, 2026-08-18). It was
   `text-align: center` until then.

   No inset, offset or padding is involved and none should be added: the
   tagline is a grid item spanning the panel's full content width, and the
   logo is the first flex item of .brand-panel__head, which is another such
   item. Both therefore start at the same content-box edge, and plain
   `text-align: left` puts the first glyph exactly on the logo's left edge —
   verified 105px for both at 1280px. `.brand-panel__more` below already
   lands there too, so all three read off one vertical line. If they ever
   drift apart, look for padding added to .__head or the tagline rather than
   correcting it with a margin here. */
.brand-panel__tagline {
  font: var(--type-h3); letter-spacing: -0.008em; text-align: left;
  font-size: clamp(14px, 3.7cqw, 21px);
  /* The lower half of the card's rhythm — see the note on
     .brand-panel__head. Overrides base.css's global
     `h1,h2,h3,h4 { margin: 0 0 --space-4 }`, which is where the old 16px
     came from; it was never chosen for this card. */
  margin: 0 0 var(--space-8);
}
/* Scoped to .brand-panel on purpose — .eyebrow is a global class used by
   the hero and most section headings, and this must not reach them.
   margin-bottom is zeroed because the eyebrow is now a flex item centred
   against the logo, and the global 8px would push it off that centre line;
   the head's own margin-bottom spaces the row from the tagline instead. */
.brand-panel .eyebrow { margin-bottom: 0; }
/* DORMANT since 2026-08-18: the closed card no longer renders a body
   paragraph — `b.description` moved into the dialog, which styles it with
   .brand-dialog__body instead. Retained because the removal is one line to
   undo in brand-panel.njk (and would need a fourth grid track back). */
.brand-panel__body { color: var(--neutral-600); }
/* `.brand-panel__cta` was removed on 2026-08-18 along with the button it
   styled — the card became the link and .brand-panel__more replaced it
   above. Nothing carries the class any more. */

/* FULLY DORMANT SINCE 2026-08-25, and this rule has been through every
   state — it came off the brand CARD on 2026-08-18, was picked straight
   back up by partials/brand-dialog.njk the same day, and lost THAT caller
   when the maintainer asked for "relevant icons ... not plain pills" in
   the dialog. Its last caller was the brand pages' own `brand.buys` row
   (brands/brands.njk), which took the same icons later the same day:
   "replace the chip list ... with icons similar to what is in those bento
   card modals". Both now render through .buys-list in §4.18. Nothing
   carries `.chip-list` any more.

   Retained rather than deleted: it is a generic pill row and the next
   list of short terms is one class away. NOTE this rule is separate from
   `.chip` below, which is NOT dormant — the newsroom card and the article
   layout still use it, and it was never part of this change. */
.chip-list { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0 0 var(--space-6); padding: 0; }

/* ============================================================
   4.3b — Brand dialog. The expanded form of a .brand-panel
   (maintainer, 2026-08-18, "Stripe-style expandable bento
   boxes"). A real <dialog> opened with showModal(), so the top
   layer, the focus trap, Escape and background inerting are the
   browser's job, not ours — js/brand-cards.js only adds
   backdrop-click and focus restoration.

   Homepage-only markup, but it lives here rather than in
   pages.css §5.3 because it is the same kit §4.3 component as
   the card it expands from and shares its surface, radius and
   head row. Splitting the pair across two files would mean
   editing both every time the card's chrome changes.
   ============================================================ */
.brand-dialog {
  /* UA styles for <dialog> are `position: fixed` + `margin: auto`, which
     already centres it. What is overridden here: the default 2px inset
     border, the `max-width/height` of `calc(100% - 6px|2em)`, and the
     padding — the dialog needs the card's own 10px radius and --e3 (the
     token's deepest elevation, which is what a top-layer surface takes).

     `max-height: min(84svh, ...)` with the inner block scrolling is what
     keeps a long dialog usable on a short viewport. svh, not vh: on iOS
     vh measures the URL-bar-hidden viewport, so a vh-sized dialog is
     taller than the visible area at rest and its CTA row lands under the
     browser chrome. */
  /* 700px is tuned, not picked: it is where the longer brand description
     (Sell Your Gold's, ~540 characters) stops setting so tall that the fact
     rows clip against the CTA footer, which is what happened at 620px. It
     is still inside the 68ch measure base.css sets for body copy.

     THE HEIGHT HAS BEEN BOTH SIDES OF THE CAP. It cleared 900px with no
     internal scroll when this was written on 2026-08-18; `summary` moved
     in from the homepage later that day and took it to ~879px, which
     scrolled; `summary` came back out; and the 2026-08-25 pass took it to
     a MEASURED 733px at 1280x900 — 121px of letterhead, 519px of content,
     93px of CTA row — by packing Founded / Acquired side by side and
     replacing the two-line chip row with a 4x2 icon grid. It does not
     scroll internally on a 900px-tall desktop today. Re-measure rather
     than trusting this paragraph; it has gone stale twice.

     The 900px ceiling is set so a 1080p-class screen shows the whole thing
     without scrolling (88svh there is ~950px, so this cap binds instead).
     It is content-derived and there is now ~167px of headroom under it, so
     it is not the binding constraint on anything — but do not lower it
     toward the current height, because scrolling is fine here (the CTA row
     is pinned outside the scroller) and a cap that tracks the content is a
     cap that needs editing every time the copy does. */
  width: min(700px, calc(100vw - 2 * var(--gutter)));
  max-height: min(88svh, 900px);
  padding: 0; border: 0; overflow: hidden;
  background: #fff; border-radius: var(--radius-lg); box-shadow: var(--e3);
  color: inherit;
}
/* SCOPED TO [open], and that is not decoration. The UA rule that hides a
   closed dialog is `dialog:not([open]) { display: none }` — an unscoped
   author `display: flex` here outranks it and the dialogs render inline in
   the brand grid, permanently open, on every page load. */
.brand-dialog[open] { display: flex; flex-direction: column; }
/* The scrolling half of the column. `min-height: 0` is required: a flex
   item's default `min-height: auto` refuses to shrink below its content,
   so without it this block wins the height argument against the dialog's
   max-height and the CTA row below is pushed out of view — the exact bug
   the row was moved out of here to fix. */
.brand-dialog__inner { flex: 1 1 auto; min-height: 0; padding: var(--space-8); overflow-y: auto; }
/* Backdrop. rgba over ink-900 rather than a token because no token is a
   translucent scrim; the colour itself is ink-900's channels.

   THE SCRIM WENT FROM 0.55 TO 0.42 WHEN THE BLUR LANDED (2026-08-20,
   maintainer asked for "the background blur"), and the two values are one
   change, not two. backdrop-filter samples what is painted behind the
   element and the scrim is painted on top of that result, so at 55% the
   page behind is already dark enough that a 10px blur is doing invisible
   work — the blur only reads once the scrim is light enough to see through.
   Lowering it alone would just make the dialog sit on a weaker ground;
   together they trade darkness for depth, which is the effect asked for.
   Do not raise the scrim back toward 0.55 without dropping the blur, or the
   filter is pure cost for nothing visible.

   `saturate(1.1)` is a small counter-move: a Gaussian blur averages
   neighbouring pixels and pulls colour toward grey, so the gold in the
   homepage hero and the brand logos behind the dialog go muddy without it.

   The -webkit- prefix is for Safari before 18, which shipped the property
   prefixed only. Not a new dependency — one extra declaration. Browsers
   with neither get the scrim alone, which is exactly the pre-2026-08-20
   appearance and is correct, just flatter.

   NOT SET ON THE VIEW-TRANSITION PATH SEPARATELY. The backdrop fades in
   under the root snapshot crossfade that the View Transitions API runs for
   free, which was measured (frame burst, 2026-08-20) and is already clean —
   the ghosting complaint that day was the brand-morph crossfade below, not
   this. */
.brand-dialog::backdrop {
  background: rgba(11, 14, 16, 0.42);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
  backdrop-filter: blur(10px) saturate(1.1);
}

/* THE OPEN ANIMATION — the FALLBACK half. A fade and a 4% scale-up driven
   by the `[open]` attribute the browser sets, with @starting-style
   supplying the from-state so the transition runs on the FIRST paint after
   showModal(). Without @starting-style there is no previous computed value
   to transition from and the dialog would simply appear.

   `allow-discrete` is required on `display` and `overlay`: both are
   discrete properties, and without it the element leaves the top layer the
   instant close() is called, so the closing transition is never seen.

   `html:not(.has-view-transitions)` IS LOAD-BEARING, NOT A NICETY. js/
   brand-cards.js puts that class on <html> when it can run the View
   Transitions morph instead, and the two animations must never both be
   live: a view transition snapshots the new state immediately after the
   DOM update, so a dialog still sitting at `opacity: 0` at that instant is
   captured invisible and the morph animates an empty box. The class is the
   switch between them — remove this selector and the morph breaks in every
   Chromium and Safari browser while looking fine in Firefox.

   Browsers with neither (no @starting-style, no View Transitions) get the
   end state immediately: an instant open, correct but unanimated. Same
   under prefers-reduced-motion, which base.css flattens globally and
   brand-cards.js checks before starting a morph. */
html:not(.has-view-transitions) .brand-dialog,
html:not(.has-view-transitions) .brand-dialog::backdrop {
  transition: opacity .22s ease, transform .22s ease, overlay .22s allow-discrete, display .22s allow-discrete;
  opacity: 0;
}
html:not(.has-view-transitions) .brand-dialog { transform: scale(0.96); }
html:not(.has-view-transitions) .brand-dialog[open] { opacity: 1; transform: scale(1); }
html:not(.has-view-transitions) .brand-dialog[open]::backdrop { opacity: 1; }
@starting-style {
  html:not(.has-view-transitions) .brand-dialog[open] { opacity: 0; transform: scale(0.96); }
  html:not(.has-view-transitions) .brand-dialog[open]::backdrop { opacity: 0; }
}

/* THE OPEN ANIMATION — the MORPH half, retuned 2026-08-20. js/brand-cards.js
   hands the card and the dialog the same `view-transition-name: brand-morph`
   across the DOM update; these four rules are the only thing that shapes how
   that plays. There is NO JS change behind them — the `has-view-transitions`
   switch, the name swap and the reduced-motion guard in brand-cards.js are
   all untouched.

   WHAT WAS WRONG WITH THE DEFAULTS. The API's own animation cross-fades the
   old and new snapshots across the FULL duration, and it composites them with
   `mix-blend-mode: plus-lighter` inside an isolated group. Both halves hurt
   here, because the two boxes are nothing alike: a 519x229 neutral-100 card
   against a 700x~740 white dialog. The old snapshot is stretched to the
   growing group box and stays half-visible the whole way, so a frame capture
   at ~80ms showed the Sell Your Gold logo smeared to triple width, the role
   eyebrow printed twice at two sizes, and the tagline ghosted over its own
   copy in the dialog. plus-lighter then washed the overlap pale. It read as a
   double exposure, which is what "not smooth" meant.

   THE FIX IS A TIGHT CROSSFADE, NOT A SLOWER ONE. The old card is given 120ms
   to leave, so it is gone long before the group has grown enough to stretch
   it into a smear; the new dialog fades in behind it over 280ms after a 60ms
   hold. The 60ms matters: at t=0 the group box is still exactly the card's
   size, and the dialog snapshot squeezed into a 519x229 letterbox is its own
   kind of wrong. Holding it back until the box has started opening means the
   first thing that resolves is already close to the right shape.

   `mix-blend-mode: normal` on both is what removes the pale wash over the
   overlap. With the two fades scheduled as above they only coexist between
   60ms and 120ms, and normal blending makes that window read as an ordinary
   dissolve.

   The group's own 360ms is longer than the default 250ms because it is the
   only thing still moving after 340ms — the box growth — and a decelerating
   ease needs room to land. cubic-bezier(.22,.68,.24,1) is a plain ease-out
   with a long tail: fast off the card, settling into the dialog's final
   size rather than stopping dead.

   REDUCED MOTION IS HANDLED IN JS, NOT HERE, and that is deliberate.
   base.css's global flattener is `*, *::before, *::after`, and the
   ::view-transition-* pseudo-elements live in their own tree off the document
   root — `*` does not reach them, so a rule here would be the only guard and
   it would be easy to believe base.css had it covered. brand-cards.js checks
   the query before calling startViewTransition() at all, so under reduced
   motion no snapshot is ever taken and none of these rules have anything to
   match. Do not "fix" this by adding a @media block; verify the JS guard. */
::view-transition-group(brand-morph) {
  animation-duration: .36s;
  animation-timing-function: cubic-bezier(.22, .68, .24, 1);
}
::view-transition-old(brand-morph) {
  mix-blend-mode: normal;
  animation: b79-morph-out .12s ease-out forwards;
}
::view-transition-new(brand-morph) {
  mix-blend-mode: normal;
  animation: b79-morph-in .28s ease-out .06s backwards;
}
/* `forwards` / `backwards` above are load-bearing: without them each snapshot
   snaps back to opacity 1 outside its own window and the smear returns for
   the remainder of the group's 360ms. */
@keyframes b79-morph-out { to { opacity: 0; } }
@keyframes b79-morph-in { from { opacity: 0; } }

/* Close button, pinned to the dialog's corner rather than sitting in the
   head row, so it stays put while .brand-dialog__inner scrolls under it.
   44px is the kit's minimum target (same floor as .button). */
.brand-dialog__close {
  position: absolute; top: var(--space-4); right: var(--space-4); z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0;
  background: #fff; border: 1px solid var(--neutral-200);
  border-radius: var(--radius-pill); color: var(--neutral-600); cursor: pointer;
  transition: background-color .15s ease, color .15s ease, border-color .15s ease;
}
.brand-dialog__close:hover { background: var(--neutral-100); color: var(--ink-900); border-color: var(--neutral-300); }

/* LETTERHEAD BAND (2026-08-25). Mirrors .brand-panel__head's content —
   same flex row, same wrap reason (logo ~219px + eyebrow ~130px do not
   share a line in a narrow dialog) — but it is a SIBLING of
   .brand-dialog__inner now rather than its first child, so it is the
   pinned top of the dialog's flex column and takes a tinted fill edge to
   edge. The brand's identity therefore stays on screen while the copy
   scrolls under it. Move it back inside the scroller and the fill stops
   at that block's padding, which is the whole reason it came out.

   `flex: 0 0 auto` for the reason .brand-dialog__cta needs it: the
   scroller between the two is `flex: 1 1 auto` and would otherwise take
   height off both ends.

   NO CORNER RADII HERE. .brand-dialog carries `overflow: hidden` and
   --radius-lg, which clips this band to the top corners for free.
   Restating them would be a second place to update if that radius ever
   moves.

   padding-right clears the close button's 44px plus its --space-4 inset,
   on top of the band's own --space-8. The button keeps its white fill,
   which reads as a control against --neutral-100 rather than dissolving
   into it. */
.brand-dialog__head {
  flex: 0 0 auto;
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--space-4);
  padding: var(--space-6) var(--space-8);
  padding-right: calc(var(--space-8) + 44px + var(--space-4));
  background: var(--neutral-100);
  border-bottom: 1px solid var(--neutral-200);
}
/* 72px, matching the card, and capped for the same retina reason — see the
   note on .brand-panel__logo above. Do not enlarge either one alone. */
.brand-dialog__logo { height: 72px; width: auto; }

/* PHONE: the letterhead pays for itself in identity but it is pinned, so
   every pixel of it comes off the scroller. Measured at 390x844, where the
   dialog is 342px and the CTA row already pins 150px at the other end: the
   band was 148px of 743, leaving 445px to read in. The logo drops to 52px
   and the block padding to --space-4, which brings it to ~111px.

   IT STILL WRAPS TO TWO LINES HERE AND THAT IS FINE. A 52px SYG logo is
   ~137px and the eyebrow ~110px against ~238px of usable row, so they
   cannot share a line at any logo size worth keeping — shrinking further
   to force it costs more legibility than the wrap costs height. Stacked,
   it reads as a letterhead anyway.

   .brand-dialog__inner loosens by the same step, which also widens the
   buys grid's tracks from 131px to 139px. */
@media (max-width: 39.99em) {
  .brand-dialog__head {
    padding: var(--space-4) var(--space-6);
    padding-right: calc(var(--space-6) + 44px + var(--space-3));
  }
  .brand-dialog__logo { height: 52px; }
  .brand-dialog__inner { padding: var(--space-6); }
}
.brand-dialog .eyebrow { margin-bottom: 0; }
.brand-dialog__title { font: var(--type-h3); letter-spacing: -0.008em; margin: 0 0 var(--space-4); }
/* `.brand-dialog__summary` WAS HERE and is deleted: the paragraph it styled
   came out of the dialog on 2026-08-18 (maintainer: "Remove the duplicate
   paragraph"). It was the ink-900 lede and this was the neutral-600
   supporting detail below it, the colour step being what stopped two
   near-identical paragraphs reading as one pasted twice.

   THAT IS WHY THIS IS NOW ink-900. With the lede gone this is the dialog's
   only prose, and leaving it at neutral-600 would render the sole paragraph
   in secondary grey under its own heading — de-emphasising the only thing
   there is to read. neutral-600 was correct as the second of two; it is not
   correct as the only one. */
.brand-dialog__body { color: var(--ink-900); margin-bottom: var(--space-6); }
/* The hairline and the padding above it are 2026-08-25 and belong to the
   subhead rather than to a wrapper, because there is no wrapper: this
   heading and the list under it are siblings of the paragraph above.
   --space-4 rather than --space-6 under the rule, against
   .brand-dialog__body's own --space-6 above it — 24px of air, a rule, then
   16px reads as a section opening; 24px on both sides reads as two gaps
   with a stray line between them. */
.brand-dialog__subhead {
  font: var(--type-eyebrow); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--gold-700); margin: 0 0 var(--space-4);
  border-top: 1px solid var(--neutral-200); padding-top: var(--space-4);
}

/* WHAT ... BUYS. The icon row under the subhead above is NOT defined here
   — it moved to its own §4.18 at the foot of this file on 2026-08-25, when
   partials/buys-list.njk gained its second caller and the rule stopped
   being dialog-only. `.brand-dialog__inner` supplies its column width;
   nothing else about it is the dialog's. */

/* Founded / Acquired, packed side by side (2026-08-25). They were two
   full-width hairline rows through partials/fact-rows.njk until then; the
   content is unchanged and only the shape is. See the note in
   partials/brand-dialog.njk for why this is local markup — fact-rows.njk
   emits a two-COLUMN dl and cannot pack, and partials/fact-grid.njk has
   exactly this shape but its CSS (pages.css) is tuned to /about/'s 453px
   column, down to a fixed `flex: 0 0 136px` label track that "ACQUIRED BY
   BEYOND79" overruns.

   `flex: 1 1 auto` so the two facts share the row evenly and their
   hairlines meet as one continuous rule; a narrow dialog wraps them rather
   than shrinking either under its own label.

   THE RULE IS ON THE TOP, unlike .fact-grid__item's, which is on the
   bottom for a reason that does not apply here: this block is the last
   thing in the scroller and .brand-dialog__cta's own border-top already
   closes it, so a bottom rule would double up against it. */
.brand-dialog__meta { display: flex; flex-wrap: wrap; gap: 0 var(--space-8); margin: 0; }
.brand-dialog__fact {
  flex: 1 1 auto; min-width: 0;
  border-top: 1px solid var(--neutral-200); padding-top: var(--space-3);
}
.brand-dialog__meta dt {
  font: var(--type-eyebrow); letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--neutral-600);
}
/* NOT --gold-700, which `.eyebrow` and .brand-dialog__subhead use: these
   are inert labels on a fact, not section headings, and gold-700 is also
   this site's link colour (CLAUDE.md, the marine ramp). Same reasoning
   .fact-grid dt records. */
.brand-dialog__meta dd { margin: var(--space-1) 0 0; font: var(--type-small); color: var(--ink-900); }
/* The pinned footer — the non-scrolling half of the flex column. Its own
   padding replaces .brand-dialog__inner's, which it sits outside of.
   Wraps to one button per line below ~420px rather than shrinking both
   past their labels. */
.brand-dialog__cta {
  flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0;
  padding: var(--space-6) var(--space-8);
  border-top: 1px solid var(--neutral-200); background: #fff;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}

/* ============================================================
   4.4 — Credentials band. REMOVED 2026-08-20 (maintainer), along
   with partials/credentials.njk and its two callers, /about/ and
   /responsibility/. Nothing renders `.credentials` any more, so the
   whole block is deleted rather than left dormant.

   It was a neutral-900 card holding a 4-up grid of issuing-body
   records (JBT, JVC, GIA, BBB) with 1px hairlines between cells.
   `entity.memberships` still exists in the fact record but is now
   read by no template — see responsibility.njk.

   neutral-900 is now the FOOTER'S ALONE. It was documented here as
   "reserved for this and the footer"; if a dark card is ever wanted
   again, that swatch is free, but read §4.13's note on the footer's
   96px margin-top before putting one at the foot of a page.
   ============================================================ */

/* ============================================================
   4.5 — Stat band. Numerals in Newsreader 300 with tabular
   figures. Column count is auto-fit, not a fixed 4: the band
   renders with four stats on the homepage and three on Our Story
   (whose long "$175 Million+" rendering also needs the wider track
   to stay on one line). A hard `repeat(4, 1fr)` left the three-stat
   instance with a dead fourth column and wrapped that value.

   2026-08-17, maintainer: restyled after Stripe's "backbone of
   global commerce" band — a bare number over its label, with the
   cell outlined top and bottom on hover.

   The 3px gold-500 rule that used to sit under each number is GONE,
   by request. It was the one thing keeping this from reading as the
   Stripe treatment. Do not reinstate it as a "fix" — its removal was
   the point, and it is a separate decision from the hover colour.

   The hover hairlines are --gold-500 (maintainer, 2026-08-17, revised
   from the --neutral-300 they shipped as a few hours earlier). So the
   band carries no gold at rest and gains it only on interaction.
   Purely decorative, so the 3:1 non-text contrast floor does not
   apply — but for the record gold-500 on --neutral-50 is 2.89:1,
   which is why this is fine as an accent and would NOT be fine if the
   hairline ever had to carry meaning on its own.

   The outline is drawn as a transparent border at rest, not added on
   hover, so the cell never changes size and the row does not shift
   when the pointer crosses it.

   The hover is DECORATIVE ONLY. These cells are not links and carry
   no hover-only information, so there is nothing here that a
   keyboard or touch user is denied. If a stat ever becomes a link,
   this needs a matching :focus-visible state.

   NOTE: this component is shared — the homepage and Our Story both
   render it. A change here lands on both.
   ============================================================ */
/* TWO-UP AT EVERY WIDTH BELOW 64em as of 2026-08-24. It used to be 1-up below
   40em, which cost 577px for four stats at 390px — the tallest thing on the
   homepage above the fold's second screen. The gap drops to --space-4 in the
   narrow case (below) because --space-8 leaves a 155px cell and the numerals
   do not fit it. This base declaration now covers the whole 40em–64em range
   too, not just below 40em — there is no separate min-width:40em rule any
   more, so don't re-add one. */
.stat-band { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-8); margin-block: var(--space-8); }
/* The --space-8 top margin exists to separate the band from body copy above
   it, which is how Our Story uses it. On the homepage the band now LEADS its
   section, where that margin just stacks on top of the section's own
   padding — 96px + 32px of dead space between the hero and the first
   numeral. Drop it in that position so the gap matches the section rhythm
   used everywhere else, and so the band groups with the heading it belongs
   to rather than floating between the two sections. */
.stat-band:first-child { margin-top: 0; }
/* At 390px a 2-up band gives each cell (342 − 32) / 2 = 155px, and
   "225,000+" at the numeral's 40px floor overflows it. The tighter gap buys
   8px per cell and the floor drops to 28px; the 40px ceiling here is the
   old floor, so this hands straight over to the base rule at 40em.

   `.stat-band .stat__value`, NOT a bare `.stat__value` — SPECIFICITY, NOT
   SOURCE ORDER. The base rule is at (0,1,0) and lives BELOW this block; a
   bare selector would tie and lose on source order, silently
   leaving the 40px floor in place with nothing in either rule pointing at
   the cause. (0,2,0) wins regardless of where this sits. Every .stat__value
   in src/ is inside a .stat-band, so the descendant adds no restriction.

   `font-size`, not the `font` shorthand: the shorthand would reset
   font-variant-numeric to `normal` and drop the tabular lining figures —
   the exact bug §4.13 records having shipped for two days.

   Measured 2026-08-24 — if a longer stat value is ever added, re-measure
   rather than assuming this still clears. */
@media (max-width: 39.99em) {
  .stat-band { gap: var(--space-4); }
  .stat-band .stat__value { font-size: clamp(28px, 7.5vw, 40px); }
}
/* A 3-item band in a 2-up grid leaves the third stat alone in row 2,
   column 1, with an empty cell beside it — /about/our-story/ passes three
   statKeys where the homepage passes four. Spanning the orphan across both
   tracks turns it into a centred capstone instead. `.stat` centres its own
   contents already, so no text-align is needed here.

   `:nth-child(3):last-child` matches ONLY a three-item band: it cannot fire
   on the homepage's four, and it cannot fire on a two-item band. Scoped
   below 64em because the min-width:64em auto-fit rule gives three items
   three tracks, so there is no orphan at desktop. Added 2026-08-24. */
@media (max-width: 63.99em) {
  .stat-band > .stat:nth-child(3):last-child { grid-column: 1 / -1; }
}
@media (min-width: 64em) { .stat-band { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } }
.stat {
  padding-block: var(--space-4);
  border-block: 1px solid transparent;
  transition: border-color .18s ease;
  /* Centres the whole cell — value, label and note together. Centring the
     label and note ALONE, under a left-aligned numeral, leaves the small
     text floating off-axis from the figure it describes; "centred under the
     stat" only reads as centred if the stat itself is on the same axis. */
  text-align: center;
}
.stat:hover { border-block-color: var(--gold-500); }
.stat__value {
  font: 300 clamp(40px, 4vw, 56px)/1 var(--font-display);
  font-variant-numeric: tabular-nums lining-nums;
  display: block;
}
.stat__label { font: var(--type-h4); margin: var(--space-3) 0 0; }
.stat__note { font: var(--type-small); color: var(--neutral-600); margin: 2px 0 0; }

/* ============================================================
   4.6 — Fact rows and the attributed quote.

   DORMANT SINCE 2026-08-25. partials/brand-dialog.njk was
   `.fact-rows`' only caller and now packs Founded / Acquired
   itself through .brand-dialog__meta in §4.3b. The partial and
   these rules are retained, not deleted — restoring them is one
   include — and partials/fact-grid.njk, the packed sibling that
   /about/ uses, is unaffected and still live.

   `.quote` below has been dormant since before this; the two are
   unrelated.
   ============================================================ */
.fact-rows { display: grid; gap: 0; margin: 0; }
@media (min-width: 40em) { .fact-rows { grid-template-columns: minmax(140px, 22%) 1fr; } }
.fact-rows dt { font: var(--type-h4); color: var(--neutral-600); padding: var(--space-3) 0; border-top: 1px solid var(--neutral-200); }
.fact-rows dd { margin: 0; padding: var(--space-3) 0; border-top: 1px solid var(--neutral-200); }
@media (max-width: 39.99em) { .fact-rows dd { border-top: 0; padding-top: 0; } }

.quote { border-left: 3px solid var(--gold-500); padding-left: var(--space-6); margin: var(--space-12) 0; }
.quote blockquote { font: 300 clamp(21px, 2.4vw, 28px)/1.35 var(--font-display); margin: 0; max-width: var(--measure); }
.quote figcaption { font: var(--type-small); color: var(--neutral-600); margin-top: var(--space-4); }

/* ============================================================
   4.7 — Timeline. The vertical rail below is the BASE. It renders on
   no page by itself: `partials/timeline.njk` is included from
   `index.njk` and nowhere else, and that one caller opts into a
   variant at every width. Our Story used to carry the base rail too,
   but that page now builds its history from per-year sections
   instead. The base is kept because a second caller arrives with it,
   not with a variant, and because each variant is a delta on it.

   FOUR VARIANTS, of which exactly TWO are live (2026-08-24):

   `.timeline--horizontal` — LIVE. A row of four from 64em up
   (2026-08-14). `.timeline--grid` — LIVE since 2026-08-24, when the
   maintainer asked for side-scrolling elements to be removed
   site-wide and index.njk moved off `timelineRail`. A 2x2 grid below
   64em, i.e. exactly the range the row of four does not cover — see
   its own ruleset comment further down for the specificity treatment
   that keeps it winning against `.timeline--horizontal`'s own
   non-media rules. index.njk sets both flags, so every width still
   gets a side-on treatment and only the mechanism differs.

   `.timeline--rail` — DORMANT since 2026-08-24, when index.njk moved
   off it (see above). Retained on the same precedent as
   `.timeline--ladder` below. It was a horizontal scroll-snap rail
   below 64em (2026-08-19), i.e. exactly the range the row of four
   does not cover.

   `.timeline--ladder` — DORMANT since the day it was added. It ran
   the milestones either side of a centre spine, which fixed the
   measure the row of four constrains (4-up is ~28 characters a line,
   the reason the body copy is --type-small) at the cost of doubling
   the band's height. The maintainer replaced the row of four with it
   and reversed that within the same session — "the timeline takes up
   wayyy too much space ... It looked better as a horizontal row to be
   honest" — and the height is now handled by collapsing the band
   behind a disclosure instead (pages.css §5.15). The block is
   retained the way §4.8's `.person` and tokens.css's marine ramp are.
   THE MEASURE ARGUMENT FOR IT WAS HEARD AND OUTWEIGHED — do not
   re-propose the ladder on that basis.
   (CORRECTED 2026-08-21: this used to add "`src/` is not under version
   control, so deleting it would be unrecoverable". That was true when
   written and is not now — `src/` is tracked. The retention still
   stands on the precedent above, which never depended on it.)
   ============================================================ */
.timeline { list-style: none; margin: 0; padding: 0 0 0 var(--space-6); border-left: 2px solid var(--gold-500); }
@media (max-width: 39.99em) { .timeline { padding-left: 12px; } }
.timeline li { position: relative; padding: 0 0 var(--space-12) var(--space-6); }
@media (max-width: 39.99em) { .timeline li { padding-left: var(--space-4); } }
.timeline li::before {
  content: ""; position: absolute; left: calc(var(--space-6) * -1 - 5px); top: 6px;
  width: 8px; height: 8px; border-radius: var(--radius-pill); background: var(--gold-500);
}
@media (max-width: 39.99em) { .timeline li::before { left: -17px; } }
.timeline__year { font: 600 19px/1.3 var(--font-body); color: var(--gold-700); display: block; font-variant-numeric: tabular-nums lining-nums; }
.section--dark .timeline__year { color: var(--gold-300); }
.timeline__title { font: var(--type-h4); display: block; margin-top: 2px; }

/* ------------------------------------------------------------
   Milestone treatment for the homepage instance (2026-08-18,
   maintainer request to restructure how the band guides the eye).
   These rules apply at EVERY width, not only where the row of four
   engages: the year is what carries the reading order, and it does
   that on a phone's vertical rail as much as on the desktop row.
   The one tier that IS width-dependent is the body copy — see the
   64em block, where the ~250px column is the whole reason it drops a
   step. At phone widths the column is full-bleed and 17px body is
   still the right size there.
   ------------------------------------------------------------ */
.timeline--horizontal .timeline__year {
  /* Display serif, the same family as the section h2 above it, so the
     four years read as chapter headings of one story rather than as
     data labels. -0.018em is --type-h1's tracking, the nearest size on
     the display scale. The 32px clamp floor is what keeps
     "2014–Present" on one line at 390px. */
  font: 300 clamp(32px, 3.4vw, 44px)/1.05 var(--font-display);
  letter-spacing: -0.018em;
  /* RESTATED, not redundant, and this line is a FIX (2026-08-20): the
     `font` shorthand resets every font-variant longhand to `normal`,
     so this rule at (0,2,0) had been silently beating the base
     `.timeline__year`'s `font-variant-numeric` at (0,1,0) and dropping
     the tabular lining figures ever since it was written on
     2026-08-18. pages.css's `.band__year` restates it for exactly this
     reason and describes itself as this rule verbatim; until now that
     claim was false in one declaration. */
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--gold-300);
  margin-bottom: var(--space-3);
}
/* "Today" is the destination, so it is brighter — but NOT a different
   size, which would break the row. (0,3,0) beats the
   `.section--dark .timeline__year` rule above it. */
.timeline--horizontal li:last-child .timeline__year { color: var(--gold-100); }
/* Base gives the title `margin-top: 2px`, which ran all three tiers
   together into one dense block. Real separation on both sides. */
.timeline--horizontal .timeline__title { margin-top: 0; margin-bottom: var(--space-3); }

/* Horizontal variant: ONE row of four, or nothing.
   The rail was each item's own `border-top`, butted together with
   `column-gap: 0`, until 2026-08-18 — done that way to avoid
   hard-coding a rail offset. It is now a `::before` on the <ol>, which
   that objection no longer covers: the pseudo-element sits at the
   <ol>'s own `top: 0` and the items' `padding-top` clears it, so there
   is no offset to guess. The reversal buys three things four borders
   could not: a graduated dim→bright ramp across the band instead of
   one flat gold line, a fade-out past the last marker so the rail
   stops reading as a divider that merely ends, and a real
   `column-gap` in place of the `padding-right` gutter hack.
   Single breakpoint at 64em, matching base.css's `.grid--4`. There is
   deliberately no 2-up middle step (there was one until 2026-08-14; the
   maintainer asked for a straight row of four instead). Below 64em this
   falls back to the vertical rail rather than wrapping, because four
   columns only fit where there is room for them: at 40em each column
   would leave about 116px of text width, and these bodies run 250-300
   characters. */
/* DORMANT since 2026-08-24 — see §4.7's summary above. index.njk no
   longer sets `timelineRail`; it sets `timelineGrid` instead (block
   further down). Nothing below is wrong, it is simply not the
   arrangement the band uses. The rest of this comment is preserved
   as written, in the tense it was written in, for whoever restores
   the flag — read "sets both flags" and "today's win" below as
   describing the state that held while the rail was live, not now.

   Below 64em the base vertical rail stacked four 250-300 character
   entries and cost 2,318px at 390px — the single largest section on
   the homepage. `.timeline--rail` laid those same four entries on a
   horizontal scroll-snap rail instead. It covered exactly the range
   `.timeline--horizontal` does not (that variant starts at 64em), so
   while it was live the homepage set both flags and every width got
   a side-on treatment; only the mechanism differed.

   The <ol> carries tabindex="0" from the partial. That was required
   here and NOT on .grid--rail: these <li>s hold no focusable content,
   so without it the scroller cannot be reached or scrolled by keyboard
   at all. See partials/timeline.njk.

   Specificity, not source order: `.timeline` and `.timeline--rail` are
   both single classes on the same <ol>, so a plain `.timeline--rail`
   selector only TIES with the base `.timeline` rules it needs to beat
   — (0,1,0) vs (0,1,0) for `padding`/`border-left` on the <ol> (line
   ~1071, plus that rule's own `padding-left: 12px` copy at
   max-width:39.99em), (0,1,1) vs (0,1,1) for `padding` on `.timeline
   li` (line ~1073, plus its own 39.99em copy), and (0,1,1) vs (0,1,1)
   for `left`/`top` on `.timeline li::before` (line ~1075, plus its
   max-width:39.99em `left: -17px` copy). A tie is broken by source
   order, so while this was live the win was only because this block
   sat after those rules in the file — reorder the two and the four
   markers would have regressed 17px left of the gold rule at ≤390px
   with no build error. The three doubled selectors below
   (`.timeline.timeline--rail`, `.timeline.timeline--rail li`,
   `.timeline.timeline--rail li::before`) raise exactly those
   contested declarations to (0,2,0)/(0,2,1), which beats every one of
   those base rules regardless of order. Do not downgrade any of the
   three back to a single class if this is ever reactivated. Everything
   else in this block (`display`, the grid props, `border-top`, `li p`,
   `:focus-visible`) has no base-rule counterpart to tie with, so a
   plain `.timeline--rail` was correct and not order-dependent for
   those.

   No `scroll-behavior: smooth`, for the reason given in §4.15. */
@media (max-width: 63.99em) {
  .timeline--rail {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 82%;
    gap: var(--space-8);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    border-top: 2px solid var(--gold-500);
  }
  /* The base rule hangs the entries off a left border with a 24px
     inset. Side-on there is no single rail to hang from, so the
     border becomes a top rule across the whole track and the inset
     padding goes back to zero. Doubled selector: see the block
     comment above — ties with `.timeline`'s own padding/border-left
     otherwise, and the win must not depend on source order. */
  .timeline.timeline--rail {
    padding: var(--space-6) 0 var(--space-4);
    border-left: 0;
  }
  .timeline--rail li {
    scroll-snap-align: start;
  }
  /* Cancels the base rule's 48px bottom and 24px left, both of which
     are for a vertical stack. Doubled selector for the same
     source-order reason as the <ol> padding above. */
  .timeline.timeline--rail li {
    padding: 0;
  }
  /* The dot moves from the left rail to the top rule. The <ol> (not the
     <li>) carries the padding here — `padding: var(--space-6) 0
     var(--space-4)` above, while `.timeline.timeline--rail li` zeroes
     the <li>'s own padding — so every <li>'s border box starts
     `--space-6` (24px) below the gold `border-top`, and the offset has
     to cancel exactly that, not center an 8px marker on a 2px border
     the way the old comment claimed. It must NOT go more negative than
     that to visually center the dot on the rule: `overflow-x: auto`
     above makes this <ol> a scroll container, and a scroll container's
     clip region is its PADDING box — anything positioned above that
     edge is clipped and will not render. Flush-below-the-rule is as
     close as this layout can get. Doubled selector: ties with
     `.timeline li::before`'s own left/top otherwise — see the block
     comment above. */
  .timeline.timeline--rail li::before { left: 0; top: calc(var(--space-6) * -1); }
  .timeline--rail li p { font: var(--type-small); margin-bottom: 0; }
  .timeline--rail:focus-visible { outline: var(--focus-ring); outline-offset: 4px; }
}
/* Below 64em the four milestones lay out 2 x 2 with their body copy
   dropped. REPLACES .timeline--rail (2026-08-24, maintainer: remove
   side-scrolling elements site-wide); that variant is now dormant.

   NO tabindex — see partials/timeline.njk. The rail needed one because it
   was a scroll container whose children hold nothing focusable. A grid is
   not, so adding one would be a focus stop that does nothing.

   THE YEAR DROPS TO 24px HERE, and that is a real constraint rather than a
   preference: .timeline--horizontal sets the year to
   clamp(32px, 3.4vw, 44px), and components.css records that the 32px floor
   exists specifically to keep "2014-Present" on ONE line at 390px. That was
   measured against a 342px full-width column. In a 2-up cell the column is
   ~163px and 32px wraps it. 24px is what fits. Re-measure if a longer year
   label is ever added.

   SPECIFICITY, NOT SOURCE ORDER — the same trap §4.13's rail block
   documents at length. `.timeline` and `.timeline--grid` are both single
   classes on the same <ol>, so a plain `.timeline--grid` only TIES with the
   base rules it must beat: `padding`/`border-left` on the <ol> and its
   max-width:39.99em copy, and `padding` on `.timeline li` and ITS
   max-width:39.99em copy. The doubled `.timeline.timeline--grid` selectors
   below raise exactly those contested declarations so the win cannot
   depend on where this block sits in the file. Do not downgrade them to a
   single class.

   TWO MORE TIES, CROSS-VARIANT rather than against the base: index.njk
   sets BOTH `timelineHorizontal` and `timelineGrid`, so below 64em the
   <ol> carries both classes. `.timeline--horizontal .timeline__year`
   (not itself in any media query — it sets the display-serif treatment
   at every width) and `.timeline--grid .timeline__year` are both
   (0,2,0); same for the two `.timeline__title` rules. A plain
   `.timeline--grid .timeline__year` would only WIN today because this
   block sits after `.timeline--horizontal`'s non-media rules in the
   file — reorder the two and the mobile year silently reverts to
   `clamp(32px, 3.4vw, 44px)`, wrapping "2014-Present" in a ~163px cell,
   with no build error. The year and title rules below are therefore
   compounded on BOTH classes (`.timeline--horizontal.timeline--grid`),
   raising them to (0,3,0), which beats `.timeline--horizontal`'s
   (0,2,0) regardless of source order. Do not downgrade either back to
   a single `.timeline--grid`.

   `.timeline--horizontal li:last-child .timeline__year { color:
   var(--gold-100) }` (0,3,1) still applies here too — the grid sets no
   `color` of its own on `.timeline__year`, so the fourth milestone
   stays the brighter "destination" gold in the grid exactly as it does
   in the row of four. That is intentional carryover, not an oversight.

   The body copy is hidden with `display: none`, not clipped: these <li>s
   are not links and have no accessible name to protect, and the full
   narrative lives on /about/our-story/, which the section's own CTA points
   at. Nothing is lost, only relocated — the same test §5.16 applies to the
   Expertise cards. */
@media (max-width: 63.99em) {
  .timeline--grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-6) var(--space-4);
  }
  /* Cancels the base vertical rail's left border and rail inset, but
     keeps the top padding: `.timeline--rail` (now dormant, §4.13)
     contributed `padding-top: var(--space-6)` here, and
     `.story-reveal__body`'s own 16px (pages.css §5.15) was written to
     land at 40px combined with it. Dropping to `padding: 0` would have
     silently halved that gap to 16px; restoring space-6 keeps the
     40px rhythm pages.css documents regardless of which variant is
     live below 64em. */
  .timeline.timeline--grid { padding: var(--space-6) 0 0; border-left: 0; }
  /* Cancels the base <li>'s 48px bottom / 24px left, both for a stack. */
  .timeline.timeline--grid li { padding: 0; }
  /* The base dot hangs off a left rail that no longer exists. */
  .timeline.timeline--grid li::before { display: none; }
  /* Compounded on `.timeline--horizontal` too — see the block comment's
     "TWO MORE TIES" paragraph above. A plain `.timeline--grid` selector
     here would tie with `.timeline--horizontal .timeline__year` and win
     only by source order. */
  .timeline--horizontal.timeline--grid .timeline__year {
    font: 300 24px/1.1 var(--font-display);
    letter-spacing: -0.018em;
    /* RESTATED, not redundant: the `font` shorthand resets every
       font-variant longhand to `normal`, so without this the tabular
       lining figures are silently dropped — the exact bug §4.13 records
       having shipped for two days in .timeline--horizontal. */
    font-variant-numeric: tabular-nums lining-nums;
    margin-bottom: var(--space-2);
  }
  /* Compounded for the same cross-variant tie as .timeline__year above. */
  .timeline--horizontal.timeline--grid .timeline__title { margin-top: 0; margin-bottom: 0; font: var(--type-h4); }
  .timeline--grid li p { display: none; }
}
@media (min-width: 64em) {
  .timeline--horizontal {
    position: relative;
    display: grid; grid-template-columns: repeat(4, 1fr);
    /* Three tiers — year, title, body — which the items adopt as a
       subgrid so all four bodies start on ONE baseline however far the
       titles wrap. Before this, titles wrapping to 1/3/1/2 lines
       started the four paragraphs at four different heights and the
       eye had no horizontal line to travel along, which is the point
       of a side-on rail. */
    grid-template-rows: auto auto auto;
    column-gap: var(--space-8);
    padding: 0; border-left: 0;
    /* The vertical rail sits inline in the text column, so it needed no
       block margin. Side-on it is a full-bleed rule across the container
       and reads as a divider — it needs air on both sides, or it crowds
       the intro paragraph above and the "Explore Our Story" link below. */
    margin-block: var(--space-8);
  }
  /* The graduated track. gold-700 → gold-500 → gold-300 is the ramp,
     held through the fourth column and then faded out: 72%-88% is
     about where the "Today" marker sits once the three column gaps are
     counted, so the rail reaches full brightness AT the last milestone
     and trails off after it rather than stopping dead at the container
     edge. */
  .timeline--horizontal::before {
    content: ""; position: absolute; left: 0; right: 0; top: 0; height: 2px;
    background: linear-gradient(90deg,
      var(--gold-700) 0%, var(--gold-500) 45%, var(--gold-300) 72%,
      var(--gold-300) 88%, transparent 100%);
  }
  .timeline--horizontal li {
    display: grid; grid-template-rows: subgrid; grid-row: span 3;
    /* The padding-top is here rather than on the <ol> so that each
       item's border box stays flush with the rail — that is what lets
       the markers below keep the same small offsets the vertical rail
       used. Where subgrid is unsupported the items lay out exactly as
       they did before it: ragged bodies, nothing broken. Assumes ONE
       <p> per row, true of all four `entity.timeline` entries; a
       second paragraph would need a fourth row declared above. */
    padding: var(--space-6) 0 0;
    border-top: 0;
  }
  /* Markers become a stepper rather than four identical dots: the three
     history steps are hollow, "Today" is solid with an offset ring —
     completed-steps → you-are-here, two states instead of four
     arbitrary sizes. The rail spans y = 0..2 of the item's border box,
     so its centre is y = 1: -4px centres the 10px marker on it, -5px
     the 12px one. The --ink-900 fill assumes the dark surface this
     variant ships on (.section--dark, homepage); on a light section it
     would need that surface's colour instead. */
  .timeline--horizontal li::before {
    left: 0; top: -4px; width: 10px; height: 10px;
    background: var(--ink-900); border: 2px solid var(--gold-500);
  }
  .timeline--horizontal li:last-child::before {
    top: -5px; width: 12px; height: 12px;
    background: var(--gold-300); border-color: var(--gold-300);
    /* Two shadows, not one: an ink gap, then the ring. */
    box-shadow: 0 0 0 3px var(--ink-900), 0 0 0 5px var(--gold-700);
  }
  /* A step down from --type-body, which is the tier contrast the bold
     16px title never had against 17px body. At 4-up each column is
     about 250px, where 14.5px actually gains ~5 characters a line.
     --type-small is also the floor tokens.css allows (NEVER below
     14px). */
  .timeline--horizontal li p { font: var(--type-small); }
  .timeline--horizontal li p:last-child { margin-bottom: 0; }
}
/* ------------------------------------------------------------
   END OF THE LIVE VARIANTS. Everything from here to the start of
   §4.8 is DORMANT — see the section header. It has no caller:
   index.njk sets `timelineHorizontal` + `timelineGrid`, not
   `timelineLadder`. Nothing below is wrong; it is simply not the
   arrangement the band uses. The rest of the comment is preserved as
   written, in the present tense it was written in, for whoever
   restores the flag.
   ------------------------------------------------------------ */

/* ------------------------------------------------------------
   `.timeline--ladder` — homepage Our Story band, added and retired
   2026-08-20. Milestones alternate either side of a centre spine
   from 64em up and fold to a single left-rail stack below it.

   WHY IT REPLACED THE ROW OF FOUR: at 4-up each column was about
   224px, which is ~28 characters a line and is the only reason the
   body copy had been stepped down to --type-small. A half-width card
   is ~470px of text, so the body goes BACK to --type-body (17px) at
   roughly 58ch and the title steps up from --type-h4 to --type-h3,
   which the 44px display year needs above it. Reversing that
   4-up decision was the maintainer's call — see index.njk.

   The alternation is `:nth-child()`, not markup: the partial emits
   the same three tiers it always did, DOM order is still reading
   order, and the <ol> keeps its native list semantics at every width.
   Nothing here scrolls, so unlike `.timeline--rail` this needs no
   `tabindex` and no `aria-label`.

   GEOMETRY, stated once so the four offsets below are checkable
   rather than magic. Markers are 12px, so their radius is 6px, and
   the spine is 2px, so its half-width is 1px.
     - Below 64em the <ol> is padded 48px on the left and the spine
       sits at x = 11..13 (centre 12). A marker at `left: -42px` on a
       card whose left edge is x = 48 therefore spans x = 6..18 —
       centre 12, on the spine. Its stub runs x = 18..48: `left:
       -30px; width: 30px`, i.e. marker edge to card edge exactly.
     - At <=40em those three numbers shrink to 32 / 5 / -32 / -20 for
       the same reason `.timeline` itself tightens there: at 390px
       the 48px inset plus 24px card padding left only ~246px of text
       (~33 characters), and 32 + 16 leaves ~278px (~37).
     - From 64em the spine is at `left: 50%` and each card is
       `calc(50% - 48px)` wide, so there is a 48px gap between a
       card's spine-facing edge and the spine. A marker at 54px past
       that edge spans edge+42..edge+54, centre edge+48 — the spine.
       Its stub is the 42px between them.
   All four sets meet the spine because 6 + 42 = 48 and 6 + 30 = 36 +
   12; change the inset and every offset in its group moves with it.

   The --ink-900 marker fill and the ink gap in the last marker's
   double box-shadow assume the dark surface this ships on
   (.section--dark, homepage), exactly as the dormant variant above
   did. On a light section both need that surface's colour instead.
   ------------------------------------------------------------ */
/* Doubled selector, for the reason set out at length in the
   `.timeline--rail` block above: `.timeline` and `.timeline--ladder`
   are both single classes on the same <ol>, so a plain
   `.timeline--ladder` only TIES with the base `padding`/`border-left`
   (and with the base's own `padding-left: 12px` copy at 39.99em) and
   would win on source order alone. (0,2,0) beats both outright. */
/* 640px is a MEASURE cap, not a taste one, and it only binds in the
   single-column mode (the 64em block below drops it). The container
   is 852px wide at 900px viewport, which after the 48px inset and
   48px of card padding left 756px of 17px body copy — about 82
   characters a line, well past the 68ch the site sets in tokens.css.
   640 leaves ~544px, or about 59 characters. Below ~688px viewport
   the container is narrower than the cap and it does nothing. */
.timeline.timeline--ladder {
  position: relative;
  padding: 0 0 0 var(--space-12);
  border-left: 0;
  margin-block: var(--space-12);
  max-width: 640px;
}
/* The spine. Same dim -> bright ramp the row of four used, rotated:
   gold-700 -> gold-500 -> gold-300, held through the last milestone
   and then faded out, so the spine reaches full brightness AT "Today"
   and trails off rather than stopping dead at the last card's bottom
   edge. The four cards are near enough equal height that the last
   marker lands around 78% of the track. */
.timeline--ladder::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 11px; width: 2px;
  background: linear-gradient(180deg,
    var(--gold-700) 0%, var(--gold-500) 45%, var(--gold-300) 72%,
    var(--gold-300) 82%, transparent 100%);
}
/* Each milestone becomes a card, which is what gives the eye a
   boundary the row of four never had: ink-800 is the token's "dark
   cards" surface and ink-700 its hairline, both against the band's
   ink-900. Doubled selector again — the base sets `padding` on
   `.timeline li` at (0,1,1), plus its own 39.99em copy. */
.timeline.timeline--ladder li {
  padding: var(--space-6);
  background: var(--ink-800);
  border: 1px solid var(--ink-700);
  border-radius: var(--radius-lg);
}
.timeline--ladder li + li { margin-top: var(--space-8); }
/* Doubled selector: contests `left`/`top` on `.timeline li::before`
   (0,1,1) and its 39.99em `left: -17px` copy. */
.timeline.timeline--ladder li::before {
  left: -42px; top: calc(var(--space-8) + var(--space-1));
  width: 12px; height: 12px;
  background: var(--ink-900); border: 2px solid var(--gold-500);
}
/* The stub from the spine to the card. `top` is the marker's top plus
   5px, which centres a 2px rule on a 12px dot. */
.timeline--ladder li::after {
  content: ""; position: absolute;
  left: -30px; width: 30px; height: 2px;
  top: calc(var(--space-8) + var(--space-1) + 5px);
  background: var(--gold-500);
}
/* "Today" is the destination: solid, ringed, and the only bright
   year — the two states the row of four established, kept. (0,3,0)
   and (0,2,1) beat `.section--dark .timeline__year` and the marker
   rule above respectively. */
.timeline--ladder li:last-child::before {
  background: var(--gold-300); border-color: var(--gold-300);
  box-shadow: 0 0 0 3px var(--ink-900), 0 0 0 5px var(--gold-700);
}
.timeline--ladder li:last-child .timeline__year { color: var(--gold-100); }
/* Three tiers with real separation, same rationale as the dormant
   variant's: display serif year so the four read as chapter headings
   rather than data labels, -0.018em being --type-h1's tracking at the
   nearest size on the display scale, and a 32px clamp floor because
   that is what holds "2014-Present" on one line at 390px. */
.timeline--ladder .timeline__year {
  font: 300 clamp(32px, 3.4vw, 44px)/1.05 var(--font-display);
  letter-spacing: -0.018em;
  /* RESTATED, not redundant. The `font` shorthand resets every
     font-variant longhand to `normal`, so this declaration at (0,2,0)
     would otherwise silently beat the base `.timeline__year`'s
     `font-variant-numeric` at (0,1,0) and drop the tabular lining
     figures. pages.css's `.band__year` restates it for exactly this
     reason and the three rules are meant to be identical. The live
     `.timeline--horizontal` copy above carried this bug from
     2026-08-18 until 2026-08-20; it is fixed there too. */
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--gold-300);
  margin-bottom: var(--space-2);
}
/* --type-h3, not the base --type-h4: 16px under a 44px year is not a
   tier, it is a gap. letter-spacing is restated because the `font`
   shorthand does not carry it. White against the section's
   neutral-200 body copy is the second half of that separation. */
.timeline--ladder .timeline__title {
  font: var(--type-h3); letter-spacing: -0.008em;
  color: #fff; margin-top: 0; margin-bottom: var(--space-4);
}
/* The tier break, drawn rather than spaced: 16px, a 40px gold rule,
   16px. Cheaper than more whitespace and it reads as a deliberate
   division of the card. */
.timeline--ladder .timeline__title::after {
  content: ""; display: block;
  width: 40px; height: 1px; margin-top: var(--space-4);
  background: var(--gold-500);
}
.timeline--ladder li p:last-child { margin-bottom: 0; }

/* Tighter inset at phone widths — see the GEOMETRY note above for why
   these four numbers move together, and what the text width would be
   if they did not. */
@media (max-width: 39.99em) {
  .timeline.timeline--ladder { padding-left: var(--space-8); }
  .timeline--ladder::before { left: 5px; }
  .timeline.timeline--ladder li { padding: var(--space-4); }
  .timeline.timeline--ladder li::before { left: -32px; }
  .timeline--ladder li::after { left: -20px; width: 20px; }
}

/* The ladder proper. `display: block` on the <ol>, NOT grid: with a
   two-column grid the auto-placement algorithm fills row 1 column 2
   before opening row 2, so items 1 and 2 would sit side by side and
   the result is a 2x2 grid, not a ladder. Explicit `grid-row` per
   item would fix that at the cost of one hard-coded rule per
   milestone — brittle the moment entity.timeline gains a fifth entry.
   Half-width block-level <li>s alternating their left margin need no
   such rule and work for any number of milestones. */
@media (min-width: 64em) {
  /* The 640px single-column measure cap is off here: each card is a
     half-column and gets its measure from that instead. At the 1200px
     container ceiling that is a 552px card, ~504px of text, about 55
     characters — inside 68ch without needing a cap. */
  .timeline.timeline--ladder { padding: 0; max-width: none; }
  .timeline--ladder::before { left: 50%; margin-left: -1px; }
  .timeline.timeline--ladder li { width: calc(50% - var(--space-12)); }
  /* -120px is a deliberate overlap, not a spacing accident: it lifts
     each right-hand card so its top sits about a third of the way up
     the left-hand card beside it, which is what makes the two columns
     read as one interlocking descent rather than two independent
     stacks. It also takes ~240px off the band.
     IT CANNOT COLLIDE AT FOUR ENTRIES, and the check is worth stating
     because a fifth would need re-checking: consecutive cards are in
     opposite halves and so cannot overlap horizontally, and the
     nearest SAME-side pair (1 and 3) is separated by card 2's full
     height less 120px — card 2 is ~350px at 1280 and ~390px at 1024,
     so ~230px of clearance at worst. A future milestone with a body
     short enough to make a card under ~150px tall would eat that.
     It also overrides the `li + li` top margin above, which is why
     the even rule restates margin-top rather than adding to it. */
  .timeline--ladder li:nth-child(even) {
    margin-left: calc(50% + var(--space-12));
    margin-top: -120px;
  }
  /* Odd cards sit left of the spine, so their marker and stub hang off
     the RIGHT edge; `left: auto` cancels the single-column offsets. */
  .timeline.timeline--ladder li:nth-child(odd)::before { left: auto; right: -54px; }
  .timeline--ladder li:nth-child(odd)::after { left: auto; right: -42px; width: 42px; }
  .timeline.timeline--ladder li:nth-child(even)::before { left: -54px; }
  .timeline--ladder li:nth-child(even)::after { left: -42px; width: 42px; }
}

/* ============================================================
   4.8 — Leadership cards. Built for 4-up, degrades to 2-up then
   1-up. Cards with a bio link through to an individual layout;
   cards without stay inert rather than opening an empty page.

   DORMANT SINCE 2026-08-19. No template renders `.person` any
   more: /about/leadership/ was the only page that ever did, and
   its overhaul replaced the card with the portrait rail in
   pages.css §5.11. The four rules below are retained the way
   tokens.css retains the marine ramp — the kit is the spec, and a
   roster of four officers is exactly what this component is for
   if one is ever supplied. Do not build a new one; use this.

   WHAT WENT: `.person--lead`, the full-bio variant — the fixed
   220px photo column, the +sibling margin and the capped bio
   measure. The rail supersedes all three (the measure now comes
   from `.band__body`'s own column), and unlike the roster card
   there is no future in which a page wants that variant back.
   ============================================================ */
.person { background: #fff; border: 1px solid var(--neutral-200); border-radius: var(--radius-lg); padding: var(--space-6); }
.person__photo { aspect-ratio: 1; border-radius: var(--radius-lg); display: grid; place-items: center; margin-bottom: var(--space-4); }
.person__name { font: var(--type-h3); margin: 0; }
.person__title { font: var(--type-small); color: var(--neutral-600); text-transform: uppercase; letter-spacing: 0.16em; }

/* ============================================================
   4.9 — Newsroom card, breadcrumb, chip. Every card carries a
   date, a category and an authored excerpt (front matter
   `excerpt`, never auto-truncated).
   ============================================================ */
.news-card { display: grid; gap: var(--space-3); padding-block: var(--space-8); border-bottom: 1px solid var(--neutral-200); }
.news-card__body { min-width: 0; display: grid; gap: var(--space-3); }
.news-card__meta { display: flex; gap: var(--space-3); align-items: center; font: var(--type-small); color: var(--neutral-600); }
.news-card__title { font: var(--type-h3); margin: 0; }
.news-card__title a { color: var(--ink-900); text-decoration: none; }
.news-card__title a:hover { text-decoration: underline; }
.news-card__excerpt { font: var(--type-small); color: var(--neutral-600); margin: 0; }
.news-card__more { margin: 0; }
/* The <a> is what gets tapped, not the <p>. 2026-08-24. */
.news-card__more a { display: inline-flex; align-items: center; min-height: 44px; }

.breadcrumb { font: var(--type-small); padding-block: var(--space-4); }
.breadcrumb ol { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; padding: 0; }
.breadcrumb li { display: flex; align-items: center; gap: var(--space-2); }
.breadcrumb li:not(:last-child)::after { content: "/"; color: var(--neutral-300); }
.breadcrumb a { color: var(--gold-700); text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb [aria-current="page"] { color: var(--neutral-600); }
.chip {
  display: inline-block; font: var(--type-small);
  border: 1px solid var(--neutral-300); border-radius: var(--radius-pill);
  padding: 2px var(--space-3);
  background: var(--neutral-100); color: var(--neutral-700);
}

/* ============================================================
   4.10 — Inline SVG icons. Base rule for every icon the `svg`
   shortcode (eleventy.config.js) emits — 8 Expertise/Responsibility
   card names + "arrow-external" at the 30px card grid, plus "arrow"
   at its own small size. Stroke/fill/caps live once here, not per
   path, per kit §4.10: "28px inline SVG, 1.4px gold-500 stroke — so
   the two grids read as one family." gold-500 is legal here because
   icons are non-text (tokens.css); never use it for text.
   ============================================================ */
.icon {
  fill: none;
  stroke: var(--gold-500);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}

/* .icon--arrow: the CTA glyph replacing literal "&rarr;" in link text
   (see eleventy.config.js). Sized in its own 18px viewBox, not the 30px
   card grid, so it sits correctly beside --type-body/--type-h4 text. */
.icon--arrow {
  margin-left: var(--space-1);
  vertical-align: -3px;
  /* NOTE: this vertical-align is inert inside `.news-card__more a` and
     `.reveal-list__more a` (both `inline-flex` since the 44px tap-target
     pass) — `vertical-align` has no effect on flex items, `align-items`
     does that job there instead. No visual defect; documented so a future
     nudge to this value isn't chased through those two callers for no
     reason. */
}

/* .icon--inline: "arrow" and "arrow-external" sit next to text and must
   read as part of that text's color, not the fixed gold-500 the 8 static
   card icons use — ink-900 inside .button--primary, gold-700 beside a
   plain link, gold-300 on ink. (Those first two were white and marine-500
   until 2026-08-18, when marine came off buttons and text.) Does not touch
   the base .icon rule: the card icons must stay gold-500. */
.icon--inline {
  stroke: currentColor;
}

/* ============================================================
   4.10 — Responsibility tiles. "Same card language as the
   Expertise functions — 28px inline SVG, 1.4px gold-500 stroke —
   so the two grids read as one family." One shared class serves
   both grids; Task 12 wraps N `.tile` includes in `.grid.grid--4`
   (the same 4-up/2-up/1-up primitive the credentials band used
   until it was removed on 2026-08-20 — see §4.4).
   ============================================================ */
.tile {
  background: #fff; border: 1px solid var(--neutral-200); border-radius: var(--radius-lg);
  padding: var(--space-8); display: grid; gap: var(--space-4); align-content: start;
  position: relative; overflow: hidden;
}
/* Gold accent rule across the card BOTTOM (maintainer, 2026-08-17 — it
   sat at the top when this was first built). The grid's only note of
   color, and the cheapest way to stop four white boxes reading as a
   table. A ::before rather than `border-bottom: 2px solid`, because a 2px
   gold edge against 1px neutral sides miters diagonally across the 10px
   radius and muddies both corners; the bar sits inside the border box
   instead and `overflow: hidden` clips it to the curve cleanly. gold-500
   is legal here for the same reason the icons are — a rule is non-text
   (tokens.css). The element's own focus ring is an outline and is NOT
   clipped by its own overflow, so a.tile still shows focus.

   `position: absolute` is load-bearing beyond placement: it takes the
   pseudo-element out of flow, so it never becomes a fourth grid item and
   cannot disturb the three-track subgrid below. */
.tile::before { content: ""; position: absolute; inset: auto 0 0 0; height: 2px; background: var(--gold-500); }
/* The icon in a gold-100 disc. Previously the 30px SVG floated bare at
   the card's top-left, where it read as a stray mark rather than a
   deliberate element — this is the single biggest visual gain here and
   it costs nothing in layout. 52px around a 30px icon leaves 11px of
   breathing room; `justify-self: start` does two jobs — it keeps the disc
   from stretching to the full grid column, and it holds it flush left so
   it lines up with the title and body beneath it.

   The disc was briefly centred earlier on 2026-08-17 and un-centred the
   same day (maintainer). Left is the resting state; do not re-centre it
   without asking. This class is shared by the Expertise grid and the
   Responsibility tiles — both homepage-only — so the two stay one
   family and a change here lands on both. */
.tile__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 52px; height: 52px; border-radius: var(--radius-pill);
  background: var(--gold-100); justify-self: start;
}
/* `text-wrap: balance` is for the long titles — "Ecommerce & Precious
   Metals Technology" otherwise drops a one-word third line at 4-up. */
.tile__title { font: var(--type-h3); letter-spacing: -0.008em; margin: 0; text-wrap: balance; }
/* --type-small, not --type-body. At 4-up a tile column is ~212px wide,
   which is ~28 characters per line at 17px against the site's own 68ch
   measure in base.css — that narrowness, not the styling, is what made
   the Expertise grid read like newsprint. 14.5px moves it to ~33 and is
   already the card-scale type the footer NAP uses. neutral-600 matches
   .brand-panel__body, so a card body reads the same everywhere;
   tokens.css documents it at 7.6:1 on white, which clears AA for small
   text with room to spare. */
.tile p { margin: 0; max-width: none; font: var(--type-small); color: var(--neutral-600); }
a.tile { text-decoration: none; color: inherit; transition: box-shadow .15s ease, border-color .15s ease; }
a.tile:hover { box-shadow: var(--e2); border-color: var(--neutral-300); }

/* --- Row alignment across a row of tiles -------------------------------
   Without this, each tile lays its own three rows out independently, so a
   two-line title and a three-line title push their paragraphs to
   different heights and the four cards read as ragged. `subgrid` hands
   the tile's row track sizing back to the parent grid, so the icon discs,
   the titles and the body copy each share one track across the whole row
   and line up exactly.

   SCOPED WITH :has(> .tile), NOT PLAIN .grid--4. The credentials band
   was the reason — it also used `.grid.grid--4` with <li> children and
   its own internal layout, so a row template on every .grid--4 reached
   into it. That band was deleted on 2026-08-20 (§4.4), so `.tile` is
   currently the only child .grid--4 ever takes and the guard is not
   earning anything today. KEEP IT ANYWAY: it costs nothing, and it is
   what makes .grid--4 safe to reuse for something that is not a tile —
   which is exactly how it was used before. Any browser new enough for
   `subgrid` has `:has()`, so the @supports guard covers both.

   The row counts are the card rows the grid actually produces: 2-up from
   40em is two card rows of three tracks (repeat(2, ...)); 4-up from 64em
   is a single card row of three. Below 40em `.grid--compact` (§4.15a) makes
   this two columns, not one, but it is still left alone here: the fixed
   52px icon track keeps titles aligned regardless of subgrid, so there is
   no misalignment to fix at that width, just no row template to add.

   The three tracks are icon / title / body. Body is `1fr` so it absorbs
   the leftover height of an equalized card — which is why the shorter
   cards keep their empty space at the bottom rather than growing a gap
   above the paragraph.

   `row-gap` must be restated: a subgrid inherits its parent's gap for the
   subgridded axis, which here is the grid's --space-8 between cards, far
   too much between an icon and its heading.

   Where subgrid is unsupported the @supports block is skipped whole and
   tiles keep their own auto rows — the pre-2026-08-17 behavior, ragged
   but not broken. */
@supports (grid-template-rows: subgrid) {
  @media (min-width: 40em) {
    .grid--4:has(> .tile) { grid-template-rows: repeat(2, auto auto 1fr); }
    .grid--4 > .tile { grid-row: span 3; grid-template-rows: subgrid; row-gap: var(--space-4); }
    /* The title track is as tall as the LONGEST title in the row — three
       lines, set by "Ecommerce & Precious Metals Technology" on Expertise
       and "Sustainability & Circular Economy" on Responsibility. Left to
       stretch, a two-line title fills that track from the top and leaves
       its slack directly above its own paragraph, so the four cards read
       as ragged even though every track lines up: the gap between heading
       and body was ~28px on some cards and ~55px on others.

       `align-self: end` moves the slack ABOVE the title instead, so the
       last line of every heading sits the same distance off the body copy
       and the four title blocks share a bottom edge. The tradeoff is that
       the icon-to-title gap now varies — but that is empty space next to a
       disc, which reads as padding, where the old gap sat between two
       blocks of text and read as a break. Icon discs, body copy and card
       edges are unaffected; they still align on their own tracks. */
    .grid--4 > .tile > .tile__title { align-self: end; }
  }
  @media (min-width: 64em) {
    .grid--4:has(> .tile) { grid-template-rows: auto auto 1fr; }
  }
  /* Same treatment for the 3-up grid, added 2026-08-18 for /about/'s six
     differentiators. base.css takes .grid--3 to two columns at 40em and
     three at 64em, so six tiles are three card rows and then two — hence
     repeat(3, ...) and repeat(2, ...) where the 4-up rule needs repeat(2,
     ...) and a single row.

     THESE ROW COUNTS ARE TILE-COUNT-DEPENDENT, exactly as the .grid--4
     rule above is: they are written for the six tiles that grid holds. A
     seventh tile would spill into a fourth 2-up row with no track template
     and lose its alignment. If the count changes, change these two
     numbers with it. */
  @media (min-width: 40em) {
    .grid--3:has(> .tile) { grid-template-rows: repeat(3, auto auto 1fr); }
    .grid--3 > .tile { grid-row: span 3; grid-template-rows: subgrid; row-gap: var(--space-4); }
    .grid--3 > .tile > .tile__title { align-self: end; }
  }
  @media (min-width: 64em) {
    .grid--3:has(> .tile) { grid-template-rows: repeat(2, auto auto 1fr); }
  }
}

/* ============================================================
   4.11 — Footer. The compliance strip is now the copyright line
   alone; it stays at 14.5px (--type-small), never smaller. This
   is the ONLY home for .site-footer rules — do not reintroduce
   them in base.css.
   ============================================================ */
/* --grad-hero + .graticule since 2026-08-25 (maintainer: "add in that
   homepage hero background to the footer background + closing section
   container background"). tokens.css's note on the token asked for exactly
   this pairing — "a fifth dark surface should take both" — so the gradient
   and the grid arrive together rather than the gradient alone.

   NOTHING STRUCTURAL WAS NEEDED HERE. §4.16's grid is an absolutely
   positioned ::before, which paints in a later pass than in-flow content
   and would cover every word; the fix is its `.graticule > .container`
   promotion, and this footer already wraps both of its halves
   (`__grid` and `__compliance`) in a `.container`. The class was added to
   <footer> in partials/footer.njk and that was the whole change. A future
   footer row added OUTSIDE a `.container` would paint under the grid.

   CONTRAST RE-MEASURED, AND ONE NUMBER MOVED. The footer used to be flat
   --ink-900; it is now a gradient whose 100% stop is #2a2214, a good deal
   lighter. Measured against that worst stop rather than against ink:

     #fff headings            15.70:1
     --neutral-200            12.25:1
     --neutral-300 body        9.99:1
     --gold-300 links          9.50:1
     --neutral-400 compliance  6.32:1   <- was 7.79:1 on flat ink-900

   Everything clears AA (4.5:1) with room. The compliance line is the one
   that changed materially: at --type-small it is normal-size text, so its
   AAA threshold is 7:1, and it now sits just under that at the warm end of
   the gradient. AA still holds. THE TOKEN IS UNCHANGED AND SHOULD STAY
   UNCHANGED — this is recorded the same way §4.12's --neutral-600 note is,
   because the fix is to stop making a 7:1 claim for this pairing, not to
   repaint the text. pages.css's dormant dark-surface block still cites
   --neutral-400 as "7.8:1"; that figure is now true only of the flat
   ink-900 surfaces, not of the four --grad-hero ones. */
.site-footer { background: var(--grad-hero); color: var(--neutral-300); padding-block: var(--space-16); margin-top: var(--space-24); }
/* 140px, not 160px, and the 20px matters: two tracks at the old floor needed
   160 + 32(gap) + 160 = 352px, and the mobile container is 342px — the grid
   missed 2-up by ten pixels and stacked all four columns, costing 920px of
   the footer's 1,160px on every page. At 140px two tracks need 312px and fit
   at 390px. At desktop widths nothing changes — 4-up either way, and the wider floor
   was never what produced the desktop 4-up. It does move the auto-fit track
   thresholds: 4-up now engages at ~704px viewport rather than ~784px (gap
   32px, gutter 24px/side). That band is strictly better — the old floor
   showed 3-up there with the fourth column orphaned on its own row — but it
   is a real difference above 40em, not a no-op. Measured 2026-08-24. */
.site-footer__grid { display: grid; gap: var(--space-8); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.site-footer a { color: var(--gold-300); display: flex; align-items: center; min-height: 44px; text-decoration: none; padding-block: var(--space-1); }
.site-footer a:hover { text-decoration: underline; }
.site-footer__heading { font: var(--type-h4); color: #fff; margin-bottom: var(--space-3); }
.site-footer__nap { font-style: normal; font: var(--type-small); color: var(--neutral-300); margin-top: var(--space-4); }
.site-footer__nap a { display: inline-flex; align-items: center; min-height: 44px; }
.site-footer__compliance { margin-top: var(--space-12); border-top: 1px solid var(--ink-700); padding-top: var(--space-6); }
.site-footer__compliance p { font: var(--type-small); color: var(--neutral-400); max-width: none; }

/* --- The three link groups as <details> (2026-08-25) -----------------
   Below 64em each group is a 56px accordion row in a single centered
   column; at 64em and up it is the plain column the footer has always
   been. These base rules are the DESKTOP half — the accordion is built
   in the max-width query below, matching this section's existing
   desktop-first shape.

   Desktop renders open because js/footer-accordion.js sets the `open`
   attribute, NOT because of anything here. If that script is absent the
   groups render collapsed and every heading is still a working
   disclosure control; see the script's own header for why the pure-CSS
   `::details-content` alternative was rejected.

   `.site-footer__heading` keeps its own `margin-bottom: var(--space-3)`
   inside the <summary>, which is a block box at these widths — so the
   desktop gap between a heading and its first link is unchanged from
   the pre-accordion footer. The mobile query zeroes it.

   BOTH marker declarations are required: `list-style` covers Firefox
   and Chrome, and Safari only drops the triangle for the -webkit
   pseudo-element. Same pair as .reveal-list__summary (§4.14). */
.site-footer__summary { list-style: none; }
.site-footer__summary::-webkit-details-marker { display: none; }
/* No pointer affordance above the breakpoint: the summary is a heading
   there, not a control. The script also re-opens a group if it is
   toggled shut at desktop width, so this is cosmetic honesty about a
   click that will not do anything rather than the thing preventing it. */
.site-footer__summary { cursor: default; }
.site-footer__toggle { display: none; }

@media (max-width: 63.99em) {
  /* One centered column. `gap: 0` because the rows carry their own
     hairlines and the identity block its own margin — an --space-8 grid
     gap on top of both would double every seam. */
  .site-footer__grid { grid-template-columns: 1fr; gap: 0; text-align: center; }
  /* THE LOGO STAYS ON TOP; THE NAP MOVES BELOW THE ACCORDIONS
     (maintainer, 2026-08-25). `display: contents` is what makes that
     possible without touching the markup: it drops the wrapper's own box
     so the <img> and the <address> become grid items in their own right
     and can be ordered separately. The wrapper is a plain div carrying no
     role, which is the case `display: contents` is safe for.

     Two consequences, both handled below and neither optional. The
     wrapper's margin-bottom goes with its box, so that spacing moves onto
     the image. And a grid item is blockified and stretched by the default
     `justify-items: stretch` — an <img> so stretched abandons its 150px
     intrinsic width for the full 342px column, so `justify-self: center`
     is load-bearing sizing here, not just alignment.

     FOCUS ORDER: `order` moves the box, not the tab sequence, so a
     keyboard user reaches the phone and email links before the three
     group headers while reading them after. Accepted deliberately — the
     footer is a flat list of links with nothing state-dependent in it, so
     meaning and operability both survive the difference (WCAG 2.4.3). The
     alternative, moving <address> after the groups in footer.njk, only
     relocates the same mismatch onto desktop, where the NAP would then be
     tabbed last while sitting in the first column. */
  .site-footer__col--identity { display: contents; }
  .site-footer__col--identity > img { justify-self: center; margin-bottom: var(--space-8); }
  /* order: 1 against the groups' default 0 — this is the only item that
     needs a value, and the DOM order of everything else already holds. */
  .site-footer__nap { order: 1; margin-top: 0; padding-block: var(--space-6); }
  /* The last group needs a closing hairline now that the NAP follows it.
     Until this change the compliance strip's own border-top sat directly
     beneath the Legal row and did that job; the NAP is now between them.
     `:last-of-type` is safe here because the three groups are the only
     <details> in the footer. */
  .site-footer__group:last-of-type { border-bottom: 1px solid var(--ink-700); }
  /* .site-footer a is `display: flex` (above), so centering the text of
     the parent is not enough — the flex line itself has to center. The
     NAP links are `inline-flex` and already centered by `text-align`;
     this is harmless on them. */
  .site-footer a { justify-content: center; }
  /* margin-top: 0 overrides the base rule's --space-12. That 48px is
     desktop rhythm between a block of columns and the copyright; against
     a stack of hairline rows it reads as the Legal row having lost its
     bottom border, because the rule meant to close the stack floats 48px
     clear of it. At 0 the compliance border-top IS that closing hairline,
     which is why no group carries a border-bottom. The strip keeps its
     own --space-6 padding-top, so the copyright does not crowd the rule. */
  .site-footer__compliance { text-align: center; margin-top: 0; }

  /* --ink-700, the same dark hairline the compliance strip uses. Only
     `border-top` per group: the compliance strip's own border-top is
     what closes the last row, so there is no doubled 2px seam anywhere
     down the stack. */
  .site-footer__group { border-top: 1px solid var(--ink-700); }
  /* A COLUMN, not a row (2026-08-25, maintainer: chevron "below the
     accordion headers"). The chevron was a plus pinned to the right edge
     until that date, which is why the earlier revision needed
     `position: relative` here and absolute positioning on the toggle —
     both are gone, because a centred mark stacked under the label is
     just the second item in a centred column and needs no positioning at
     all. Do not reintroduce the right-edge anchor to "save a row of
     height"; the stacked chevron is the requested form.

     `align-items: center` is what centres the chevron, which is an 18px
     item on a full-width flex line — `text-align` on the grid does not
     reach it. 56px is kept as a FLOOR rather than the height: the stack
     now measures taller than that, and the value stays only so a row can
     never fall under the touch-target minimum. */
  .site-footer__summary {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: var(--space-1); min-height: 56px; padding-block: var(--space-3);
    cursor: pointer;
  }
  .site-footer__summary .site-footer__heading { margin-bottom: 0; }
  /* Restated rather than left to base.css's global rule: <summary> is the
     focusable element while the visible row is its parent, so without an
     explicit negative offset the outline hugs the summary's content box
     and clips against the row's hairline. Same reasoning as
     .reveal-list__summary:focus-visible. */
  .site-footer__summary:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }

  /* No positioning: this is the column's second item and centres itself.
     gold-300 is the only gold legal on ink (tokens.css) and is what the
     footer's links already use — the mark takes it through
     `icon--inline`'s currentColor. */
  .site-footer__toggle { display: inline-flex; color: var(--gold-300); }
  .site-footer__toggle .icon { transition: transform .15s ease; }
  /* 180, not 45: this is a chevron, so the open state points it back the
     way it came. `.site-nav__chevron` (§4.1) does the same thing on the
     nav's dropdowns, and the two affordances should not disagree. */
  .site-footer__group[open] .site-footer__toggle .icon { transform: rotate(180deg); }

  .site-footer__links { padding-bottom: var(--space-4); }
}

/* The footer's 96px top margin and 64px padding are desktop rhythm.
   On a 390px screen that is 160px of empty ink above the first link — 96px
   margin plus 64px padding — and 224px of margin-and-padding in total, on
   top of a footer that is already the tallest single element on short pages
   (53% of /news/). Both land on --space-12: --space-24 drops two steps on
   the 4/8/12/16/24/32/48/64/96/128 scale (96 -> 64 -> 48), --space-16 drops
   one (64 -> 48) — so the rhythm stays token-derived even though the two
   started different distances away. Added 2026-08-24. */
@media (max-width: 63.99em) {
  .site-footer { padding-block: var(--space-12); margin-top: var(--space-12); }
}

/* ============================================================
   4.12 — Editorial band. Two-column section: a heading (and, if
   the page wants one, an icon) in a sticky left column, prose
   right at --measure.

   PROMOTED FROM pages.css §5.9 ON 2026-08-18, at that section's
   own instruction ("If a second page ever wants `.resp-band`,
   promote it and rename it then"). /responsibility/ built it;
   /about/ is the second page, so `.resp-band` became `.band`
   and `.resp-standfirst` became `.standfirst`. The rules below
   are that code unchanged apart from the names — do not treat
   the move as licence to retune it.

   WHAT DID NOT COME WITH IT: the sticky offset and the :target
   scroll-margins. Both were computed from Responsibility's tab
   bar, which is that page's furniture and not this component's.
   The sticky top is now the --band-sticky-top custom property,
   defaulting to the header island (80px) + --space-8 of air —
   correct for a page with nothing under the header. A page that
   puts something there overrides it; see pages.css §5.9.
   ============================================================ */

/* The band. 5fr/7fr, not 1fr/1fr: the prose column has to hold
   --measure (68ch ≈ 620px) without the aside squeezing it. At the
   1200px container that is 453px / 635px after the 48px gap; at the
   64em breakpoint itself, 380px / 532px — where the longest aside
   heading ("Sustainability & Circular Economy") still sets on two
   lines at the full 34px h2, so no smaller heading step is needed.

   64em, matching .grid--4 and the hero split, is also the width
   below which there is no spare column to put an aside in — under
   it the aside simply stacks above its prose. */
.band { display: grid; gap: var(--space-8); }
@media (min-width: 64em) {
  .band {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    column-gap: var(--space-12);
  }
}

/* Sticky aside — the page's wayfinding, and the reason this needs no
   scroll-spy JS. The heading of the area you are reading stays on
   screen for the whole area.

   `align-self: start` is load-bearing: a grid item stretches to the
   row height by default, and a full-height item has nothing to slide
   within, so sticky silently does nothing without it.

   112px = the 80px header island + --space-8 of air, so a stuck
   heading sits clear of the header instead of butting against it.
   Sticky only engages at 64em (1024px), which is above the 900px
   where the island is 80px rather than 73px, so this is one number
   and not a pair. IF THE HEADER IS RESIZED, THIS MOVES WITH IT. No
   z-index: anything the page floats above the band (a tab bar, say)
   must keep painting over a heading sliding up under it, and leaving
   this at auto is what guarantees that. */
.band__aside { --band-sticky-top: calc(80px + var(--space-8)); }
@media (min-width: 64em) {
  .band__aside { position: sticky; top: var(--band-sticky-top); align-self: start; }
}
/* `--aside-heading-offset` WAS DECLARED HERE and is deleted (2026-08-19).
   It measured how far a row's true top sat above its <h2>, for the anchor
   clearances in pages.css to add — necessary while the section icon was a
   block stacked over the heading, and 46px then. The icon moved inside the
   h2 on 2026-08-18, which made the h2 the top of its row again and the
   value 0.

   It is gone rather than parked at 0 because the coupling was the hazard:
   declared here, consumed by three `:target` declarations in pages.css, so
   removing it without simplifying them first makes all three invalid at
   computed-value time and drops every anchor on two pages to the top of the
   viewport, silently. Those three now carry plain numbers and this file no
   longer participates. If an aside ever stacks something above its heading
   again, adjust them there — see the note above the tiers in pages.css. */
.band__aside h2 { margin-bottom: 0; }
/* The icon is the per-section marker where a page uses one — see
   responsibility.njk's note on why 01–04 numbering was rejected.
   `display: block` because the shortcode emits a bare inline <svg>. */
/* THE ICON TRAILS THE HEADING TEXT, INLINE — the default since 2026-08-18,
   when /responsibility/ adopted the treatment /expertise/ had, leaving no
   page that stacks its icon above the heading. Before that this was
   `display: block` plus a --space-4 bottom margin, and each of the two pages
   carried an identical override in pages.css; promoted here and both scoped
   copies deleted, per the CLAUDE.md §6 two-page rule.

   The SVG sits INSIDE the <h2> in the markup, not as a sibling before it —
   see either page's template. That is what lets the icon follow a wrapped
   heading's last word; a sibling can only sit beside the heading's whole
   block. The shortcode already emits aria-hidden="true", so the heading's
   accessible name is unaffected.

   `.heading-mark` binds a heading's final word to its icon so the pair
   cannot be split across a line break. Without it the icon wraps alone
   whenever the last line is nearly full, stranding it on a line by itself —
   which is exactly what "Secure Operations & Logistics" did at 34px in a 5fr
   aside. The spans are authored per heading in the templates, not derived:
   only the author knows which word is last once a heading is reworded.

   EXCEPTION, 2026-08-19: src/brands/brands.njk derives the mark from the
   heading's final word, because its headings come from entity.json and
   there is no per-heading template markup to author. That template accepts
   an optional `headingMark` key per section for the cases where the icon
   should bind to more than the last word, which preserves the author
   control this paragraph is actually protecting. Verified correct for all
   six `layout: "band"` headings across both brands — the two
   `layout: "closing-card"` sections render a plain `<h2>` with no
   `.heading-mark` span at all, so they are outside this derivation
   entirely. */
.band__aside .icon {
  display: inline-block;
  vertical-align: middle;
  margin: 0 0 0 var(--space-2);
}
.band__aside .heading-mark { white-space: nowrap; }

/* BODY TONE AND PARAGRAPH RHYTHM, 2026-08-24. Two defects, both measured
   (see docs/superpowers/specs/2026-08-24-editorial-typography-design.md §2.2):

   1. Paragraph space was --space-4 (16px) against 28.05px leading — 0.57x,
      under the 0.75-1.0x editorial norm. --space-6 is 0.86x. The standfirst
      moves up a step so it still out-spaces a body run that now equals its
      old value.
   2. `body` and `.standfirst` were BOTH --ink-900, so the standfirst was
      distinguished by face, size and weight but shared the fourth axis with
      the copy it leads. It now differs on all four.

   THE TONE IS SET ON THE CONTAINER, NOT ON `p`, and that is load-bearing.
   A descendant rule like `.band__body p` (0,1,1) outranks `.standfirst`
   (0,1,0) and `.expertise-callout` (0,1,0) and would repaint both
   --neutral-700. Inheritance loses to any direct rule, so setting it here
   leaves every element that declares its own colour alone, with no :not()
   chain to keep in sync as devices are added. Tasks that add elements in
   here (`.spec`, `.band__summary`) declare --ink-900 directly and are
   correct by construction.

   --neutral-700, NOT --neutral-600: see tokens.css's corrected note. 600
   measures 6.79:1 on the muted bands, under AAA. 700 holds 10.38:1 on the
   worst surface this component sits on, and 11.13:1 on --neutral-50. */
.band__body { color: var(--neutral-700); }
.band__body p { margin-bottom: var(--space-6); }
.band__body .standfirst { margin-bottom: var(--space-8); }
/* `:last-child` counts in the class column, so `.band__body > p:last-child`
   is (0,2,1) — higher than `.band__body p`'s (0,1,1). It wins the last
   paragraph back to a zero margin on SPECIFICITY, not source order; the
   position after `.band__body p` is for reading order only and could move
   without changing behavior.
   Consequence: `.band__body .standfirst` is (0,2,0), which LOSES to this
   rule's (0,2,1). No section today ends a band with its `.standfirst` —
   it is always promoted from the opening sentence (see the per-page
   section comments) — so this has never fired. If one ever did, its
   `.standfirst` would silently collapse to `margin-bottom: 0` instead of
   `var(--space-8)`, undoing the promotion's spacing with no visible error
   until someone measured it. */
.band__body > p:last-child { margin-bottom: 0; }

/* Standfirst: a section's own opening sentence, promoted. The type
   is not a new value — it is `.quote blockquote`'s declaration from
   §4.6 verbatim. That rule is the kit's one large display-serif body
   treatment and has been unused since it shipped (quote.njk is not
   included anywhere), so this reuses the scale rather than inventing
   a fifth one.

   NOT a .quote/<blockquote> in the markup, though: these are
   Beyond79's own statements, not quotations, and wrapping them in a
   blockquote to borrow the styling would be a semantic lie. */
.standfirst {
  font: 300 clamp(21px, 2.4vw, 28px)/1.35 var(--font-display);
  color: var(--ink-900);
}

/* Closing statement. Every section on /expertise/ and /responsibility/
   already ends with a summary sentence — "Our goal is simple: …",
   "Together, these systems allow …", "This customer-focused approach
   supports …" — and until 2026-08-24 nothing styled it, so each section
   simply stopped. Seven sections carry one: four of four on
   /responsibility/, three of four on /expertise/.

   IT IS AUTHORED PER SECTION IN THE TEMPLATE AND MUST NEVER BE DERIVED
   FROM `p:last-child`. /expertise/ §Technology's last paragraph is the
   XRF / Sigma Metalytics sentence the template marks `BACK FROM HOME` —
   the only place on the site naming those capabilities, and not a summary.
   A structural selector would dress it as one.

   This one returns to --ink-900 after the --neutral-700 body run, which is
   the signal doing most of the work; the gold dash is the secondary mark.
   gold-500 is legal here because it is a RULE, not text (tokens.css).

   Until 2026-08-25 this dash was contrasted against `.expertise-callout`'s
   full-height gold left rule — Logistics carried both devices, and a
   rule-above and a rule-left were kept from reading as the same thing. That
   left rule has since been removed at the maintainer's request (pages.css
   §5.5), so this dash is now the only gold rule in the band. */
.band__summary {
  color: var(--ink-900);
  font-weight: 500;
  margin-block-start: var(--space-8);
}
.band__summary::before {
  content: "";
  display: block;
  width: 32px; height: 2px;
  background: var(--gold-500);
  margin-bottom: var(--space-4);
}

/* ------------------------------------------------------------
   4.13 — Closing card. A page's final dark surface, as a card
   rather than a full-bleed band: the footer's site-wide 96px
   margin-top would otherwise strand light between two dark
   surfaces. Promoted out of pages.css §5.9 on 2026-08-18 when
   /expertise/ became the second page to want it.

   Geometry is `.error-card`'s (§404) so the site has one dark-card
   shape rather than three: radius-xl, ink-900. The padding is a
   step down from the 404's because that card is a whole page's
   content and this one closes a page.

   Colour comes from `.section--dark` on the same element
   (base.css); nothing is restated here. The paragraphs keep
   --measure from base.css's `p` rule, so the copy does not run the
   full 1136px.

   It deliberately does NOT use `.band`: a grid nested inside the
   card's own padding would put the columns a few px off the bands
   above it, and a near-miss alignment is worse than a clean break.
   ------------------------------------------------------------ */
/* --grad-hero + .graticule since 2026-08-25, the same pass that gave the
   footer both (§4.11) — all four callers, so /expertise/ and
   /responsibility/ do not sit flat above a gradient footer. `.section--dark`
   is still on every one of them for its typography cascade; the background
   it sets is simply overridden here, which is why nothing in the templates
   changed except the added `graticule` class.

   TWO RULES BELOW EXIST ONLY TO HOST THE GRID, and neither is cosmetic:

     `> *` gets `position: relative` for the reason §4.16 spells out — a
     positioned ::before paints in a later pass than in-flow siblings, so
     without this the grid covers the copy. §4.16 solves it with
     `.graticule > .container`, but this card has no `.container` child:
     the brand pages' child is `.closing-card__inner` and /expertise/ and
     /responsibility/ put bare `h2`/`p` here. `> *` covers all three
     without naming any of them.

     `overflow: hidden` clips the ::before to the radius. `inset: 0` on a
     square pseudo-element would otherwise paint the grid into all four
     rounded corners.
   ------------------------------------------------------------ */
.closing-card {
  border-radius: var(--radius-xl);
  padding: var(--space-12) var(--space-8);
  background: var(--grad-hero);
  overflow: hidden;
}
.closing-card > * { position: relative; }
@media (min-width: 64em) { .closing-card { padding: var(--space-16) var(--space-12); } }
.closing-card > p:last-child { margin-bottom: 0; }

/* ============================================================
   4.14 — Reveal list. The homepage Responsibility section's form
   as of 2026-08-19, and DELIBERATELY not the .tile card grid the
   Expertise section directly above it uses.

   The two sections were the same component in the same .grid--4
   until this date, per kit §4.10's "so the two grids read as one
   family". Two adjacent rows of four identical cards on one page
   read as the page repeating itself, and the maintainer asked for
   them to differ. This is the differentiated half; §4.10 is
   untouched and still serves Expertise here and /about/.

   Nothing here styles a .tile, and nothing here is reused by any
   other page — but it lives in components.css rather than pages.css
   because it is a self-contained component, and a second caller
   would arrive wanting exactly these rules.

   NO gold ::before hairline, unlike .tile. The open row's inset
   gold rule carries state instead, which is the point: the two
   sections must not share their accent device either.
   ============================================================ */
.reveal-list { border-top: 1px solid var(--neutral-200); }

/* The inset rule is declared transparent at rest and colored under
   [open], rather than added on open: a rule that appears would change
   the border box and shift every row below it by 3px on each toggle.
   Same reason padding-inline is unconditional. */
.reveal-list__item {
  border-bottom: 1px solid var(--neutral-200);
  padding-inline: var(--space-4);
  box-shadow: inset 3px 0 0 transparent;
  transition: box-shadow .15s ease, background-color .15s ease;
}
.reveal-list__item[open] {
  box-shadow: inset 3px 0 0 var(--gold-500);
  background: #fff;
}

.reveal-list__summary {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: var(--space-4); padding-block: var(--space-3);
  cursor: pointer;
  /* Removes the default disclosure triangle. BOTH declarations are
     required: `list-style: none` handles Firefox and Chrome, and Safari
     only drops the marker for the -webkit pseudo-element. */
  list-style: none;
}
.reveal-list__summary::-webkit-details-marker { display: none; }
/* The row's own focus ring. The global :focus-visible rule in base.css
   already supplies this shape; it is restated because <summary> is the
   focusable element while the visible row is its parent, and without an
   explicit rule the outline hugs the summary's content box and clips
   against the row's inset shadow. */
.reveal-list__summary:focus-visible { outline: var(--focus-ring); outline-offset: -2px; }

/* 44px around the shortcode's 30px icon, against .tile__icon's 52px. The
   disc is the row's height driver here, and 44px is chosen because that
   is the minimum touch-target size (WCAG 2.5.5/2.5.8) — not because of
   the arithmetic against the 344px card grid this replaces: a row is
   icon + 2×--space-3 padding + a 1px border, so even at 52px that is
   only 4×77+1 = 309px, still 35px shorter than 344px. 44px gives the
   tighter row and clears the touch-target floor at the same time. */
.reveal-list__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--radius-pill);
  background: var(--gold-100); flex: none;
}
.reveal-list__title {
  font: var(--type-h3); letter-spacing: -0.008em; margin: 0; text-wrap: balance;
}
/* gold-700, not gold-500: this glyph takes currentColor (icon--inline) and
   sits at 18px, which is text scale — it reads as UI beside the row's
   heading rather than as a decorative icon, so it is held to the same
   standard as the title next to it. gold-500's 3.4:1 clears WCAG
   1.4.11's non-text-contrast floor for a plain icon; gold-700 is chosen
   for how this glyph reads here, not because 3.4:1 fails the rule. */
.reveal-list__toggle {
  display: inline-flex; color: var(--gold-700);
  transition: transform .15s ease;
}
.reveal-list__item[open] .reveal-list__toggle { transform: rotate(45deg); }

/* Indented to the title's left edge, so the open body reads as belonging
   to its row rather than to the list. */
.reveal-list__body {
  padding: 0 0 var(--space-6) calc(44px + var(--space-4));
}
.reveal-list__body p {
  font: var(--type-small); color: var(--neutral-600); max-width: var(--measure);
}
.reveal-list__more { margin-bottom: 0; }
/* The <a> is what gets tapped, not the <p>. 2026-08-24. */
.reveal-list__more a { display: inline-flex; align-items: center; min-height: 44px; }

/* Below 40em the icon disc costs more than it earns: it pushes the title
   into a second line on a 390px row. The title and toggle keep the row. */
@media (max-width: 39.99em) {
  .reveal-list__summary { gap: var(--space-3); }
  .reveal-list__icon { width: 36px; height: 36px; }
  .reveal-list__icon .icon { width: 22px; height: 22px; }
  .reveal-list__body { padding-left: calc(36px + var(--space-3)); }
}

/* ============================================================
   4.15 — Grid rail. Turns a .grid--4 into a horizontal
   scroll-snap rail BELOW 40em only, where that grid is 1-up and
   four stacked full-width cards cost 1,616px on a 390px phone —
   15% of the whole page for one section.

   A MODIFIER, NEVER A CHANGE TO .grid--4. base.css's .grid--4 is a
   shared primitive — it carried the credentials band until 2026-08-20
   (§4.4) and still carries /about/; rail behaviour has to be opted
   into by the one caller that wants it.

   No `scroll-behavior: smooth` anywhere here, deliberately.
   base.css's prefers-reduced-motion block flattens animation and
   transition durations globally but does NOT cover scroll-behavior,
   so a smooth rail would keep animating for users who asked it not
   to. Not opting in is the fix.

   DORMANT SINCE 2026-08-24. Its one caller — the homepage Expertise
   grid in index.njk — moved to .grid--compact (§4.15a) when the
   maintainer asked for side-scrolling elements to be removed
   site-wide. Nothing in src/ sets this class now; grep before
   assuming otherwise. Retained, not deleted, on the same precedent
   as .timeline--ladder (§4.13) and tokens.css's marine ramp: it is
   a working component and the reversal is a preference, not a
   defect. If it is ever revived, note that it needs its trailing
   scroll-gap fix (described above) which was never implemented.
   ============================================================ */
@media (max-width: 39.99em) {
  .grid--rail {
    grid-auto-flow: column;
    grid-auto-columns: 78%;
    grid-template-columns: none;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    /* The gap is the grid's --space-8. This padding-bottom is vertical —
       it clears the scrollbar the browser draws under an overflow-x
       track so the cards don't sit flush against it, nothing to do with
       horizontal snap spacing (padding-bottom cannot affect that axis).
       It does NOT give the last card room to snap flush left; that gap
       is still open. (Note for a future editor: `scroll-padding-right`
       would NOT fix it — scroll-padding shrinks the snapport, it does
       not create trailing scrollable space, so the last card's
       start-snap position would still sit beyond max-scroll. A trailing
       `padding-right` of roughly `100% - 78%`, or an `::after` spacer,
       is the remedy that would actually work — not implemented here.) */
    padding-bottom: var(--space-4);
  }
  .grid--rail > * { scroll-snap-align: start; }
}

/* ============================================================
   4.15a — Grid compact. Turns a .grid--4 into a TWO-UP grid
   below 40em, where that grid is otherwise 1-up.

   REPLACES .grid--rail AS THE HOMEPAGE EXPERTISE TREATMENT
   (2026-08-24, maintainer: remove side-scrolling elements
   site-wide). §4.15 above is now dormant — see its note.

   THIS IS TALLER THAN THE RAIL IT REPLACES, BY DESIGN AND WITH
   THE MAINTAINER'S AGREEMENT: a rail is one row, a 2-up grid is
   two, so this costs roughly +225px on the homepage. It is still
   far shorter than the 1-up stack (~1,616px) the rail originally
   existed to avoid, and the height is bought back several times
   over by the footer and type-scale changes in the same pass.
   Do not "restore" the rail on height grounds — that trade was
   made deliberately.

   A MODIFIER, NEVER A CHANGE TO .grid--4, for the same reason
   §4.15 gives: base.css's .grid--4 is a shared primitive and
   still carries /about/.

   The gap tightens to --space-4 because --space-8 leaves a 155px
   cell at 390px; --space-4 leaves 163px, which is what keeps the
   longest title ("Ecommerce & Precious Metals Technology") to
   four lines rather than six.
   ============================================================ */
@media (max-width: 39.99em) {
  .grid--compact { grid-template-columns: repeat(2, 1fr); gap: var(--space-4); }
  /* The 21px --type-h3 title is sized for a full-width card. In a 163px
     cell it wraps to six lines and the four cards stop reading as a set.
     --type-h4 is the next step down on the scale. */
  .grid--compact .tile__title { font: var(--type-h4); }
}

/* ============================================================
   4.16 — Graticule. The 96px ink-and-gold grid the homepage hero
   has carried since the rebuild, masked to a radial bloom parked
   upper-right.

   PROMOTED HERE FROM pages.css ON 2026-08-21, when the homepage
   Our Story band asked for the hero's surface and would have been
   the motif's fourth caller (hero, one brand header per brand,
   Our Story). Both copies — §5.1 and §5.4 — were byte-identical
   apart from a missing -webkit- prefix in the hero's, and §5.4's
   own comment ended "If a fourth dark band ever wants this, that
   is the refactor." This is that refactor. A fifth surface takes
   the class; it does not take a local copy.

   IT PAIRS WITH --grad-hero, ALWAYS. Every caller paints that
   gradient underneath it. The lines are gold-500 at 10% alpha,
   which is legible on ink and invisible on cream — this class does
   not make a light section interesting, it makes a dark one deep.

   The mask position is the one knob: --graticule-at, defaulting to
   the hero's own 78% 18%. Set the property on a caller rather than
   forking the rule. BOTH mask properties are required; Safari still
   only drops the triangle for the -webkit- one.
   ============================================================ */
.graticule { position: relative; }
.graticule::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--gold-500) 10%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--gold-500) 10%, transparent) 1px, transparent 1px);
  background-size: 96px 96px;
  -webkit-mask-image: radial-gradient(120% 90% at var(--graticule-at, 78% 18%), #000 0%, transparent 72%);
          mask-image: radial-gradient(120% 90% at var(--graticule-at, 78% 18%), #000 0%, transparent 72%);
}
/* REQUIRED, NOT COSMETIC — without it the grid paints over every word in
   the band. Within one stacking context, positioned descendants with
   `z-index: auto` paint in a LATER pass than in-flow block content, so an
   absolutely positioned ::before beats its element's ordinary children
   regardless of source order. Promoting the container to positioned puts it
   in the same pass, where document order decides and it comes after.

   This was `.hero > .container` and `.brand-header > .container` before the
   promotion; all three callers wrap their content in a `.container`, so the
   child selector carries over unchanged. No z-index on either rule, so no
   stacking context is opened and the sticky header's z-index:50 still paints
   over the band. */
.graticule > .container { position: relative; }

/* ============================================================
   4.17 — Specimen grid. A list of short parallel labels, each on
   a hairline, each with an icon. Sits inside a `.band__body`, so
   it sizes off that 7fr column (635px at the 1200px container)
   rather than borrowing base.css's .grid--3 breakpoints, which
   assume the full container width.

   PROMOTED FROM pages.css §5.5 ON 2026-08-24 as `.method-grid` /
   `.method`, when /responsibility/'s four-areas index became the
   second caller. Renamed because a responsibility area is not a
   testing method.

   DELIBERATELY NOT `.tile`: no border box, no background fill and
   no tinted icon disc. /expertise/'s own template records why —
   a bordered card with a tinted disc reads as a repeat of the
   homepage Expertise tiles, which link TO this page. Adding the
   icon (2026-08-24, maintainer request) does not reopen that:
   the icon sits inline on the existing hairline row.

   COLUMN BEHAVIOUR, computed against the 635px body column and
   the --space-4 (16px) gap, not guessed:
     base  170px floor — 3-up at 635px (3x170+2x16=542; 4-up needs
                         728). 1-up at 342px (2-up needs 356).
     wide  240px floor — 2-up at 635px (2x240+16=496; 3-up needs
                         752), so a four-item index lands 2x2
                         instead of 3-up-plus-an-orphan. 1-up at
                         342px.
   1-UP AT 390px IS CORRECT, NOT A FALLBACK. At 2-up a label would
   have 125px beside a 30px icon, and "X-ray fluorescence (XRF)
   spectrometry" would run five or six lines. If the band's 5fr/7fr
   ratio or --container ever changes, BOTH floors need recomputing
   together.
   ============================================================ */
.spec-grid {
  list-style: none; margin: 0 0 var(--space-4); padding: 0;
  display: grid; gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
}
.spec-grid--wide { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
/* --ink-900 is declared here, not inherited: §4.12 sets `.band__body`
   to --neutral-700 and these labels are specimens, not running prose. */
.spec {
  display: flex; align-items: flex-start; gap: var(--space-2);
  border-top: 1px solid var(--neutral-300);
  padding-top: var(--space-2);
  font: var(--type-small); line-height: 1.4;
  color: var(--ink-900);
}

/* ============================================================
   4.18 — Buys list. `entity.brands[].buys` as a mark-and-label
   grid: partials/buys-list.njk, rendered by the homepage brand
   dialog (§4.3b) and by the brand pages' band body.

   PROMOTED FROM §4.3b ON 2026-08-25, when the maintainer asked
   for "icons similar to what is in those bento card modals" on
   the brand pages and the second caller appeared. Same rule, one
   file, so the two cannot drift — that is the whole point of the
   shared partial. The comment below is the original, extended
   with the band column's own measurements.

   DELIBERATELY NOT `.spec-grid` (§4.17), which is the other
   icon-and-label list living in a `.band__body`. That one is a
   hairline specimen index; this one has to match the dialog it
   was lifted from, because both render the SAME EIGHT LABELS and
   a reader who opens the bento card and then follows through to
   the brand page must see one object, not two treatments of it.
   Adding hairlines here to match §4.17 breaks that; do not.

   THE ICONS RENDER AT THEIR NATIVE 30px CARD-GRID SIZE and are
   not scaled down. Shrinking a 30px viewBox thins its 1.4px
   stroke under a pixel — the reason "arrow" carries its own 18px
   grid in eleventy.config.js rather than being a shrunk copy of
   the card set — and native size is also what lets this row
   reuse "jewelry", "valuation" and "industrial" unmodified. So
   the row height is the icon's, not the label's.

   auto-fit against a 126px floor, NOT a fixed column count. One
   rule covers both callers with no breakpoint of its own,
   against the --space-3 (12px) gap:
     dialog  636px inside the padding at 700px  -> 4 columns
             ~278px inside the padding at 390px -> 2 columns
     page    635px `.band__body` at the 1200px container
             ((635+12)/138 = 4.6)               -> 4 columns
             342px at 390px ((342+12)/138 = 2.5)-> 2 columns
   Sell Your Gold's eight items land 4x2 in both places and 2x4
   on a phone; Recycle Platinum's three fill one line. `1fr`
   rather than `auto` so the tracks are even and the labels start
   on a common x down each column.

   `min-width: 0` on the item is what lets a long label wrap
   INSIDE its track instead of widening it — the same auto-floor
   removal .journey__step needs (see CLAUDE.md). "Luxury watches"
   is the longest label at ~100px and is what the 126px floor was
   measured against; it is the one to re-measure if a longer term
   is ever added to `buys`.

   --ink-900 on the label is declared, not inherited: §4.12 sets
   `.band__body` to --neutral-700, and on the brand pages these
   are specimens, not running prose. §4.17's `.spec` carries the
   same declaration for the same reason.
   ============================================================ */
.buys-list {
  list-style: none; margin: 0 0 var(--space-6); padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(126px, 1fr));
  gap: var(--space-3);
}
.buys-list__item { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.buys-list__label { font: var(--type-small); color: var(--ink-900); min-width: 0; overflow-wrap: break-word; }
