/* ==========================================================
   Silver Bullet — shared site chrome

   Extracted from index.html so every page gets the same header, nav,
   bullet intro and hook banner from ONE source instead of a copy that
   drifts. Load AFTER tokens.css and textures.css.

   No cache-busters. Vercel revalidates every asset on every load via
   ETag, so a changed file reaches visitors on the next request without
   any ?v= string. See TEXTURES.md house rule 5.
   ========================================================== */

    /* ==========================================================
       Silver Bullet — page styles
       Colors come from tokens.css. No raw hex in here.
       ========================================================== */

    :root {
      /* Scale, space, radius and motion tokens live in tokens.css.
         What is left here is site-specific animation config: the intro
         timeline, marquee rates, and the reactive chrome default. */
      /* Layout */

      /* Intro timeline — every value the animation depends on */
      --t-fire:     0.10s;  /* muzzle blip */
      --t-fly:      1.00s;  /* bullet crosses the screen */
      --t-glint:    0.55s;  /* light catches it, mid-screen */
      --t-name:     0.60s;  /* wordmark wipes in */
      --t-shift:    1.15s;  /* logo slides to the left */
      --t-nav:      1.50s;  /* nav items arrive */
      /* Page fades up. Held at 1.80s originally, which meant <main> and the
         footer sat at opacity:0 from first paint until 2.35s — and Largest
         Contentful Paint cannot fire on an element that has not been
         painted, so LCP measured 2488ms on every page except the homepage.
         That is 12ms under Google's 2.5s threshold on a LOCAL server with a
         warm cache and no network; on a phone it fails outright.

         Brought forward so the page arrives while the bullet is still
         crossing rather than after it has finished. The sequence is intact
         — blip, flight, glint, wordmark, logo travel, nav — it now plays
         OVER a page that is already there, which is also why .intro-layer
         is transparent and always was. */
      --t-page:     0.35s;

      /* Slogan ticker.
         The ride is deliberately a long way. The return has to cover this
         distance in its first few hundred milliseconds to leave at anything
         like the bullet's pace, and a short ride leaves the deceleration
         nothing to be felt across — measured, 144px read as a flat crawl. */
      --ticker-ride: clamp(280px, 42vw, 620px);
      --ticker-rate:   91s;   /* row A, no-JS fallback: 3462px set / 38px/s. JS uses data-marquee-speed */
      --ticker-rate-b: 128s;  /* row B, no-JS fallback: 3854px set / 30px/s */

      /* Work conveyors. Three bands run right-to-left on this page, so they
         get three clearly different speeds — matched speeds would read as
         one striped block rather than as separate systems. 70px/s for the
         creative slots, 18px/s for the logo band, against the hook
         banner's 28px/s. */
      /* No-JS fallback, and a whole-loop time rather than a speed, so it has
         to be recomputed whenever the set gains or loses a slot: it is
         setWidth / px-per-second. 2232px of set at 1440 wide, at 70px/s = 31.9s.
         It was 36s against a 50px/s JS speed, which was already 8.6% out.
         motion.js prefers data-marquee-speed precisely because this number
         retunes itself every time the content changes. */
      --conveyor-rate: 31.9s;
      --logo-rate:    112s;   /* no-JS fallback; JS uses data-marquee-speed */

      /* Set by JS: how far left the logo must travel from centre */
      --logo-shift: 0px;

      /* Reactive chrome. 0-100. Drives the hard stop positions in the chrome
         gradients via calc(). 50 = centred band, which is also the static
         state when JS is off or motion is reduced. */
      --light: 50;
    }

    * { box-sizing: border-box; }

    /* No CSS smooth scrolling: Lenis owns scrolling when motion is on, and
       the two fight if both are active. Under reduced motion Lenis never
       initialises and anchors jump instantly, which is correct.

       scroll-padding-top is what keeps the sticky header off the top of a
       section the browser jumps to on its own: landing on a URL that already
       carries a hash, a reload while deep-linked, reduced motion, or JS off.
       Lenis handles in-page CLICKS itself with its own -72 offset in
       motion.js, so this covers only the cases Lenis never sees - which is
       exactly the set that was landing section headings under the nav. */
    html {
      scroll-behavior: auto;
      scroll-padding-top: var(--nav-h);
    }

    /* Lenis base styles (required by lenis 1.x). */
    html.lenis, html.lenis body { height: auto; }
    .lenis.lenis-smooth { scroll-behavior: auto !important; }
    .lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
    .lenis.lenis-stopped { overflow: hidden; }

    body {
      margin: 0;
      background: var(--bg);
      color: var(--text);
      font: clamp(16px, 1.1vw, calc(16px + 2px * var(--dial)))/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      overflow-x: hidden;
    }

    .wrap {
      width: 100%;
      max-width: var(--wrap);
      margin: 0 auto;
      padding: 0 var(--gutter);
    }

    a { color: inherit; }

    :where(a, button, input, textarea):focus-visible {
      outline: 2px solid var(--amber);
      outline-offset: 3px;
    }

    /* ---------- skip link ----------
       The first focusable thing on every page. Without it a keyboard or
       screen reader user tabs the wordmark, four nav links, the dropdown and
       the CTA on every single page before reaching the content.

       Parked off the top edge rather than display:none or visibility:hidden,
       either of which would take it out of the focus order and defeat the
       point. It sits above the intro layer (z-index 60) so it is reachable
       while the bullet is still playing.

       Warm surface because it sits over the header band, DESIGN.md's lit top
       edge on the leading edge, and the same amber focus ring every other
       link on the site already gets from the rule above. */
    .skip-link {
      position: fixed;
      top: 0;
      left: var(--s-3, 12px);
      z-index: 70;
      transform: translateY(-120%);
      padding: var(--s-3, 12px) var(--s-5, 20px);
      background: var(--bg-warm-raised, #17130F);
      color: var(--white, #E8F1F2);
      border: 1px solid var(--border, #323738);
      border-top-color: var(--border-lit, #4A4C4D);
      border-radius: 0 0 var(--r-sm, 8px) var(--r-sm, 8px);
      font-size: 15px;
      line-height: 1.2;
      text-decoration: none;
      transition: transform var(--dur-quick, .18s) var(--ease-enter, cubic-bezier(.25, 1, .5, 1));
    }
    .skip-link:focus { transform: translateY(0); }

    @media (prefers-reduced-motion: reduce) {
      .skip-link { transition: none; }
    }

    /* ---------- type ---------- */

    /* Display sizes were roughly 1.7x'd in the scale pass. The old ladder
       (86 / 46 / 18) read as a product page; these are billboard sizes,
       and the tighter tracking is what keeps them from going loose. */
    h1, h2, h3 {
      color: var(--text-strong);
      font-weight: 800;
      letter-spacing: calc(-0.035em - 0.01em * var(--dial));
      margin: 0;
      text-wrap: balance;
    }

    h1 {
      font-size: clamp(calc(40px + 8px * var(--dial)), calc(7vw + 2.6vw * var(--dial)), calc(86px + 66px * var(--dial)));
      line-height: calc(0.98 - 0.06 * var(--dial));
    }

    h2 {
      font-size: clamp(calc(28px + 10px * var(--dial)), calc(4vw + 2vw * var(--dial)), calc(46px + 46px * var(--dial)));
      line-height: calc(1.02 - 0.06 * var(--dial));
    }

    h3 {
      font-size: clamp(calc(18px + 2px * var(--dial)), 1.5vw, calc(18px + 6px * var(--dial)));
      line-height: 1.2;
      letter-spacing: -0.025em;
    }

    p { margin: 0; }

    .lead {
      font-size: clamp(calc(17px + 1px * var(--dial)), calc(1.5vw + 0.1vw * var(--dial)), calc(20px + 4px * var(--dial)));
      line-height: 1.5;
      color: var(--steel-300);
      max-width: 58ch;
    }

    .meta {
      font-size: calc(14px + 1px * var(--dial));
      color: var(--text-muted);
    }

    /* ---------- reactive chrome ----------
       --light slides the hard stops. It never softens them: every band is
       still a PAIR of stops at one position, the pair just moves together.
       Applied to exactly three elements — the hero headline, the bullet,
       and the machined bar above the contact heading.
       Driving a gradient from a custom property repaints that element every
       frame, so this deliberately does not scale. */

    [data-chrome] { --lo: calc((var(--light) - 50) * 0.9%); }

    /* The headline's metal is .tx-rim-text now: one soft band that leans
       toward the cursor, rather than the hard stepped bands. And it is
       hover-only - see textures.js. Previously it was driven by motion.js,
       which blends scroll in, so it moved wherever you were on the page. */

    /* The bullet sweeps less far — it is 20px tall, not 200px wide. */
    .bullet[data-chrome] { --lo: calc((var(--light) - 50) * 0.42%); }

    /* ---------- header ---------- */

    /* The header and the ticker under it are one warm band: the room the
       cold steel page sits in. DESIGN.md's premise is "cold steel subject,
       warm environment behind it", and until this the environment was the
       same cyan-pulled dark as everything else. The band is where it shows.

       Nothing in the band that is SUBJECT goes warm - the wordmark, the nav
       type, the button's label all stay on the steel ramp. Only the light
       behind them and the surfaces they sit on. */
    .site-header {
      position: sticky;
      top: 0;
      z-index: 40;
      height: var(--nav-h);
      /* NO background of its own - the pane supplies it, and an opaque fill
         here would sit UNDER the pane and give the backdrop blur nothing
         but flat colour to sample. Near-solid warm at the top of the page
         (92%), then 58% plus blur once lifted: the bar gets glassier as
         content starts passing under it, which is the "floats over the
         page" the brief asked for. */
      --tx-frost-tint: color-mix(in srgb, var(--amber-burnt) 45%, var(--taupe-dark));
      --tx-frost-base: var(--bg-warm);
      --tx-frost-rest: 92%;
    }

    .site-header .wrap {
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 24px;
    }

    /* The line the bullet leaves behind. It is the nav separator - and it
       is still drawn by the intro (rule-draw scales it from the left), so
       the element and its transform-origin stay exactly as they were. What
       changed is what it is made of: a warm hairline at ~11% that fades out
       at both ends, instead of a solid slate line edge to edge. The bullet
       still draws it; it just draws something softer. */
    .site-header__rule {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 1px;
      background: linear-gradient(90deg,
        transparent 0%,
        var(--line-warm) 12%,
        color-mix(in srgb, var(--amber) 16%, transparent) 50%,
        var(--line-warm) 88%,
        transparent 100%);
      transform-origin: left center;
    }

    .logo {
      display: inline-block;
      line-height: 0;
      text-decoration: none;
      will-change: transform;
      position: relative;
    }

    /* The wordmark is 148x12, far under any tap-target minimum. Its BOX must
       not change: intro.js measures logo.getBoundingClientRect() to compute
       --logo-shift, so padding here would move where the logo flies from.
       An absolutely positioned overlay expands only the hit area and leaves
       the anchor's own rect untouched. Safe here precisely because the logo
       has no neighbour within 44px to overlap. */
    .logo::after {
      content: "";
      position: absolute;
      left: 0; right: 0;
      top: 50%;
      transform: translateY(-50%);
      height: 44px;
    }

    /* The wipe is applied to this wrapper, so it clips whatever sits inside —
       it worked on text and it works on the image for the same reason. */
    .logo__inner { display: block; }

    .logo__img {
      display: block;
      /* A fixed width matters more than it looks. The intro measures this
         element to work out how far it has to travel, and that measurement
         happens before the image file has necessarily arrived. Without a
         width reserved in CSS the box would still be empty at that moment
         and the logo would fly to the wrong place. */
      /* The lockup is 3.3:1, so width drives height: at the old 196px cap it
         stood 59px tall inside a 72px bar, which left 6px of air. Capped to
         152px it lands at 46px and the bar breathes again. The width stays
         declared for the reason above - the intro measures it before the
         file lands - and height is now declared too, so the box is fully
         reserved and nothing reflows when the image arrives. */
      width: clamp(calc(118px + 18px * var(--dial)), calc(13vw + 1vw * var(--dial)), calc(152px + 44px * var(--dial)));
      height: auto;
      aspect-ratio: 800 / 242;
    }

    .nav {
      position: relative;        /* the wide Capabilities panel hangs off this, not its toggle */
      display: flex;
      align-items: center;
      gap: clamp(calc(18px + 2px * var(--dial)), calc(2.5vw + 0.3vw * var(--dial)), calc(34px + 8px * var(--dial)));
    }

    .nav a {
      font-size: calc(15px + 1px * var(--dial));
      color: var(--steel-300);        /* one step up from --steel-500 */
      text-decoration: none;
      transition: color var(--dur-hover) var(--ease-hover);
    }

    .nav a:hover { color: var(--steel-050); }

    /* The underline grows from the centre. Scoped to the plain links - the
       dropdown's summary and the CTA have their own signals. scaleX rather
       than animating width, so it never touches layout. Steel, not amber:
       the ticker's CTA is the one warm element in this viewport. */
    .nav > .nav-link {
      position: relative;
      padding-bottom: 3px;
    }
    .nav > .nav-link::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 1px;
      background: var(--steel-300);
      transform: scaleX(0);
      transform-origin: center;
      transition: transform var(--dur-hover) var(--ease-enter);
    }
    .nav > .nav-link:hover::after,
    .nav > .nav-link:focus-visible::after { transform: scaleX(1); }

    @media (prefers-reduced-motion: reduce) {
      .nav a,
      .nav > .nav-link::after { transition: none; }
    }

    /* ---------- the text roll ----------
       Built by roll.js: each nav label (and each client-wall mark) is
       wrapped in .roll, one line tall and clipped, holding two copies of
       itself. The first is in flow and sets the height; the second is
       positioned just below the clip. Hover or keyboard focus slides both
       up one line, so the label rolls out the top while its twin rolls in
       from below. Transforms only, so it never touches layout, and the
       wrapper takes exactly the space the bare text did.

       --roll-dur and --roll-ease are the spin; --roll-step is the per-letter
       delay used when roll.js is in stagger mode. The bezier is a hard
       launch with a settle, so it reads as one quick spin rather than a
       slide. */
    :root {
    }

    .roll {
      position: relative;
      display: inline-block;
      overflow: hidden;
      vertical-align: top;
      white-space: nowrap;
    }

    .roll__line {
      display: block;
      transition: transform var(--dur-roll) var(--ease-roll);
    }

    /* The twin: same box, one line lower, waiting under the clip. */
    .roll__line[aria-hidden] {
      position: absolute;
      top: 100%;
      left: 0;
    }

    /* Whole-word mode: the line moves. */
    .has-roll:hover .roll__line,
    .has-roll:focus-visible .roll__line { transform: translateY(-100%); }

    /* Letter mode: the line stays put and each letter moves on its own,
       --i letters after the first. inline-block so a transform applies. */
    .roll--stagger .roll__line { transition: none; }
    .roll__ch {
      display: inline-block;
      white-space: pre;
      transition: transform var(--dur-roll) var(--ease-roll);
      transition-delay: calc(var(--i, 0) * var(--roll-step));
    }
    .has-roll:hover .roll--stagger .roll__ch,
    .has-roll:focus-visible .roll--stagger .roll__ch { transform: translateY(-100%); }
    .has-roll:hover .roll--stagger .roll__line,
    .has-roll:focus-visible .roll--stagger .roll__line { transform: none; }

    /* The dropdown summaries open on hover of the whole <details>, so the
       roll keys off the same thing and matches the panel's timing. */
    .nav-dd[open] > .has-roll .roll__line { transform: translateY(-100%); }
    .nav-dd[open] > .has-roll .roll--stagger .roll__ch { transform: translateY(-100%); }
    .nav-dd[open] > .has-roll .roll--stagger .roll__line { transform: none; }

    /* The client-wall marks. Both copies are the same <img>, so the sizing
       caps below apply to each; the line has to be as tall as the image and
       no taller, which display:block on the img already gives it. */
    .roll--mark .roll__line { line-height: 0; }

    /* Reduced motion: nothing rolls. The twin stays under the clip, the
       colour and opacity hovers the nav and wall already have still fire,
       and that is the whole state change. */
    @media (prefers-reduced-motion: reduce) {
      .roll__line,
      .roll__ch { transition: none !important; transform: none !important; }
    }

    /* ---------- nav dropdown ----------
       Built on <details> so the thing works with the script absent: click
       opens it, the keyboard gets it for free, and nav.js only adds the
       enhancements — hover-open on a fine pointer, Escape, close on an
       outside click.

       Inbound's version holds seventeen services in three named groups. We
       have six capabilities and no groups, so this stays a single numbered
       column. Inventing a taxonomy to fill a wider panel would be inventing
       information. */

    /* Stretching to the header's full height is what lets the panel sit at
       top:100% and land exactly on the nav rule, instead of guessing an
       offset from the summary's own text height. */
    .nav-dd {
      position: relative;
      align-self: stretch;
      display: flex;
      align-items: center;
    }

    .nav-dd__toggle {
      display: flex;
      align-items: center;
      gap: 7px;
      font-size: calc(15px + 1px * var(--dial));
      color: var(--steel-500);
      cursor: pointer;
      list-style: none;          /* kills the disclosure triangle … */
      transition: color var(--dur-quick) var(--ease-hover);
    }
    .nav-dd__toggle::-webkit-details-marker { display: none; }  /* … and again for WebKit */

    .nav-dd__toggle:hover,
    .nav-dd[open] .nav-dd__toggle { color: var(--white); }

    /* Drawn rather than typed. A + character would sit on the text baseline
       and jump as the font resolves; two 1px bars do not. The upright bar is
       what leaves when the panel opens, so it reads + closed, - open. */
    .nav-dd__sign {
      position: relative;
      flex: none;
      width: 9px;
      height: 9px;
    }
    .nav-dd__sign::before,
    .nav-dd__sign::after {
      content: "";
      position: absolute;
      inset: 50% 0 auto 0;
      height: 1px;
      background: currentColor;
    }
    .nav-dd__sign::after {
      transform: rotate(90deg);
      transition: transform var(--dur-quick) var(--ease-hover), opacity var(--dur-quick) var(--ease-hover);
    }
    /* Scoped to the dropdown's OWN toggle. Descendant-matched, this also
       flipped the sign on every group head inside an open Capabilities
       panel, so a collapsed B2B read as "-". */
    .nav-dd[open] > .nav-dd__toggle .nav-dd__sign::after {
      transform: rotate(0deg);
      opacity: 0;
    }

    /* Separation is the rim-light move, same as .panel in tokens.css: lit top
       edge, ink bottom, no fill beyond bg-raised and no shadow. DESIGN.md
       rules out box-shadow for depth and this is the case it would be most
       tempting in. */
    .nav-dd__panel {
      position: absolute;
      top: 100%;
      right: 0;
      z-index: 50;
      min-width: calc(292px + 28px * var(--dial));
      padding: 8px;
      background: var(--bg-raised);
      border-top: 1px solid var(--border-lit);
      border-left: 1px solid var(--border);
      border-right: 1px solid var(--border);
      border-bottom: 1px solid var(--ink);
      border-radius: 0 0 var(--r-md) var(--r-md);
      overflow: hidden;
    }

    /* The panel hangs off the frosted bar, so it is frosted too - it reads as
       the same pane of glass rather than a solid card stuck to the underside
       of one. Same recipe as .tx-frost in textures.css, with more fill: the
       bar carries three words, this carries seven rows of 14px type and has
       to stay readable over whatever it happens to be covering.

       This is NOT the accidental see-through it replaces. That was the
       nightfield z-index tie letting .ticker and <main> paint over the panel;
       this is a controlled blur of what sits behind it, and the rim-light
       borders above still do the separating. Blur is a backdrop treatment,
       not a box-shadow - DESIGN.md's no-glow rule is about light bleeding
       OUT of an element, which this does not do.

       Guarded on @supports because the fallback has to be the opaque fill:
       without a working blur, a translucent menu is just hard to read. */
    @supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
      .nav-dd__panel {
        background: color-mix(in srgb, var(--bg-raised) 82%, transparent);
        -webkit-backdrop-filter: blur(14px) saturate(108%);
        backdrop-filter: blur(14px) saturate(108%);
      }

      /* Touch pays most for a backdrop blur and gains least, exactly as the
         bar reasons in textures.css. Same call, same numbers. */
      @media (hover: none), (prefers-reduced-transparency: reduce) {
        .nav-dd__panel {
          -webkit-backdrop-filter: none;
          backdrop-filter: none;
          background: color-mix(in srgb, var(--bg-raised) 96%, transparent);
        }
      }
    }

    /* Opening answers a click, which DESIGN.md allows — it shows what
       changed. Open only; <details> removes the panel on close, so there is
       nothing left to animate out and a fake close would need JS holding the
       element in the DOM for no gain. */
    .nav-dd[open] .nav-dd__panel {
      animation: dd-open .2s var(--ease-enter) both;
    }

    @keyframes dd-open {
      from { opacity: 0; transform: translateY(-6px); }
      to   { opacity: 1; transform: translateY(0); }
    }

    /* ---- the Capabilities panel: three columns and a strip ----
       Rendered by capabilities.js. Grid areas, top to bottom:

         .        brands   brands      the parent label over the two brand
         agencies b2b      b2c         columns, so the hierarchy is visible
         shared   shared   shared      the set that runs under everything,
                                       full width so it reads as a floor
                                       and not a fourth column

       Each column is a <details> held open on desktop by nav.js, so its
       summary is a plain heading here; the accordion behaviour only
       exists on a phone. The four <details> land in the areas by
       source order, which is also the numbering order (01 to 25).

       The panel is wider than a nav item, so it anchors to the nav's
       right edge rather than the toggle's: anchored to the toggle it
       runs off the left of the viewport at 1024px. Width is capped at
       the viewport less the gutters for the same reason. */
    .nav-dd--wide { position: static; }
    .nav-dd--wide .nav-dd__panel {
      right: 0;
      width: min(calc(840px + 80px * var(--dial)), calc(100vw - 2 * var(--gutter, 24px)));
      min-width: 0;
      padding: var(--s-2) var(--s-3) var(--s-3);
    }

    .nav-dd__cols {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      grid-template-areas:
        ".  brands brands"
        "g1 g2     g3"
        "sh sh     sh";
      gap: 0 var(--s-2);
    }
    .nav-dd__parent { grid-area: brands; }
    .nav-dd__group:nth-of-type(1) { grid-area: g1; }
    .nav-dd__group:nth-of-type(2) { grid-area: g2; }
    .nav-dd__group:nth-of-type(3) { grid-area: g3; }
    .nav-dd__group--shared        { grid-area: sh; }

    /* The parent label. Small and sentence-case: DESIGN.md rules out the
       tracked-out all-caps eyebrow, so the hierarchy is carried by the
       rule under it spanning both brand columns, not by letterspacing. */
    .nav-dd__parent {
      margin: 0 0 var(--s-1);
      padding: var(--s-2) var(--s-3) var(--s-1);
      border-bottom: 1px solid var(--border);
      font-size: 12px;
      font-weight: 500;
      color: var(--steel-700);
    }

    .nav-dd__group { min-width: 0; }
    .nav-dd__group + .nav-dd__group:not(.nav-dd__group--shared) {
      border-left: 1px solid var(--border);
      padding-left: var(--s-2);
    }

    .nav-dd__group-head {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--s-3);
      padding: var(--s-3) var(--s-3) 0;
      list-style: none;
      cursor: default;
    }
    .nav-dd__group-head::-webkit-details-marker { display: none; }
    .nav-dd__group-head .nav-dd__sign { display: none; }   /* no +/- on a heading */

    .nav-dd__group-name {
      font-size: calc(14px + 1px * var(--dial));
      font-weight: 600;
      letter-spacing: -.005em;
      color: var(--steel-050);
    }

    .nav-dd__group-tag {
      margin: var(--s-1) 0 var(--s-2);
      padding: 0 var(--s-3);
      font-size: 12px;
      line-height: 1.45;
      min-height: calc(2 * 1.45em);   /* two lines reserved, so the three lists start level */
      color: var(--steel-700);
    }

    /* The shared strip: the foundation layer. A rule above it, the list
       laid out across rather than down. */
    .nav-dd__group--shared {
      margin-top: var(--s-2);
      padding-top: var(--s-1);
      border-top: 1px solid var(--border);
    }
    /* Flex, not equal columns: the four names differ in length, and equal
       tracks wrapped the longest one while leaving the shortest swimming.
       Each takes its natural width and the row wraps only if it must. */
    .nav-dd__list--row {
      display: flex;
      flex-wrap: wrap;
      gap: 0 var(--s-2);
    }
    .nav-dd__list--row .cap-name { white-space: nowrap; }

    /* Resources is a plain single column. */
    .nav-dd__panel--single { min-width: 240px; }

    .nav-dd__list {
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .nav-dd__list a {
      display: flex;
      align-items: baseline;
      gap: var(--s-3);
      padding: calc(var(--s-2) + 2px * var(--dial)) var(--s-3);
      font-size: calc(14px + 1px * var(--dial));
      color: var(--steel-300);
      border-radius: var(--r-sm);
      /* One duration for the row, the text and the number, so they move
         as one thing. --dur-quick is the token for a state answering the
         pointer. */
      transition: color var(--dur-quick) var(--ease-hover),
                  background-color var(--dur-quick) var(--ease-hover);
    }

    .nav-dd__list a:hover,
    .nav-dd__list a:focus-visible {
      color: var(--white);
      background: var(--charcoal);
    }

    /* The number is the quiet part of the row and it brightens with it.
       Tabular figures in the body face, not monospace, per DESIGN.md. */
    .nav-dd__list .cap-n {
      flex: none;
      min-width: 2ch;
      font-size: 11px;
      color: var(--steel-700);
      font-variant-numeric: tabular-nums;
      transition: color var(--dur-quick) var(--ease-hover);
    }
    .nav-dd__list a:hover .cap-n,
    .nav-dd__list a:focus-visible .cap-n { color: var(--steel-100); }
    .nav-dd__list .cap-name { min-width: 0; }

    .nav-dd__all {
      display: block;
      margin-top: 6px;
      padding: calc(11px + 2px * var(--dial)) calc(12px + 2px * var(--dial));
      border-top: 1px solid var(--border);
      font-size: calc(14px + 1px * var(--dial));
      color: var(--steel-100);
    }

    .nav-dd__all:hover { color: var(--white); }

    @media (prefers-reduced-motion: reduce) {
      .nav-dd[open] .nav-dd__panel { animation: none; }
      .nav-dd__sign::after { transition: none; }
    }

    /* Under 760px the header is logo + one action. The dropdown goes with
       the links rather than becoming a second, differently-behaved menu —
       a real mobile nav is its own job, not a side effect of this one. */
    /* ---------- the hamburger ----------
       Two bars, not three: at this weight a third reads as clutter, and the
       pair rotate into a cross without needing one of them to vanish. */

    .nav-burger {
      display: none;              /* desktop has room for the real nav */
      appearance: none;
      background: none;
      border: 0;
      /* 26x16 of bars in a 46x44 target. The vertical padding is 14px, not
         the 10px the horizontal keeps, purely to clear 44px: measured 46x36
         before, which is under the 44x44 a finger is drawn against. Nothing
         moves, because .site-header has a fixed height and .site-header .wrap
         centres its children in it, so the extra 8px is taken from the space
         either side of the button rather than from the bar. The bars, the
         gutter and the crossing animation are untouched. */
      padding: 14px 10px;
      margin-right: -10px;        /* optical: the bars sit on the gutter */
      cursor: pointer;
      color: var(--steel-100);
    }
    .nav-burger:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

    .nav-burger__box {
      display: block;
      position: relative;
      width: 26px;
      height: 16px;
    }
    .nav-burger__box span {
      position: absolute;
      left: 0;
      width: 100%;
      height: 1px;               /* a hairline, like every other edge here */
      background: currentColor;
      transition: transform var(--dur-base) var(--ease-metal),
                  opacity   var(--dur-quick) linear;
    }
    .nav-burger__box span:first-child { top: 0; }
    .nav-burger__box span:last-child  { bottom: 0; }

    /* open: the two bars meet in the middle and cross */
    [aria-expanded="true"] .nav-burger__box span:first-child {
      transform: translateY(7.5px) rotate(45deg);
    }
    [aria-expanded="true"] .nav-burger__box span:last-child {
      transform: translateY(-7.5px) rotate(-45deg);
    }

    @media (max-width: 760px) {
      .nav-burger { display: block; }

      /* The panel IS the nav element - the same links, restyled. Nothing is
         duplicated into a second menu, so the two can never drift apart. */
      .nav {
        position: fixed;
        top: var(--nav-h);
        left: 0;
        right: 0;
        z-index: 39;              /* under the header, over the page */
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        padding: var(--space-sm) var(--gutter) var(--space-md);
        background: var(--bg);
        border-bottom: 1px solid var(--border);
        max-height: calc(100dvh - var(--nav-h));
        overflow-y: auto;

        /* Closed. visibility keeps it out of the tab order while still
           allowing the slide to animate. */
        visibility: hidden;
        opacity: 0;
        transform: translateY(-8px);
        transition: opacity var(--dur-base) var(--ease-metal),
                    transform var(--dur-base) var(--ease-metal),
                    visibility 0s linear var(--dur-base);
      }

      body.nav-open .nav {
        visibility: visible;
        opacity: 1;
        transform: translateY(0);
        transition-delay: 0s;
      }

      /* Links become rows with a lit top edge, matching the panels. */
      .nav > a,
      .nav .nav-dd {
        border-top: 1px solid var(--border);
      }
      .nav > a { padding: calc(16px + 2px * var(--dial)) 0; font-size: calc(17px + 3px * var(--dial)); }
      .nav .nav-link { color: var(--steel-050); }

      /* The capabilities dropdown stays a <details> here - tapping it opens
         the list in place rather than flying out a panel with nowhere to go. */
      /* Stacked, not the desktop row: toggle on top, panel underneath. */
      .nav-dd { width: 100%; flex-direction: column; align-items: stretch; }
      .nav-dd__toggle { padding: calc(16px + 2px * var(--dial)) 0; font-size: calc(17px + 3px * var(--dial)); }
      .nav-dd__panel {
        position: static;
        width: auto;
        padding: 0 0 8px;
        background: none;
        border: 0;
      }

      /* Capabilities > B2B / B2C > list. The columns become stacked
         accordions: nav.js closes them on load here, and tapping a group
         head opens its list in place. */
      /* Capabilities > a group > its list. Every group, the shared set
         included, is a stacked accordion: nav.js closes them on load
         here, and tapping a group head opens its list in place. */
      .nav-dd--wide { position: relative; }
      .nav-dd--wide .nav-dd__panel { width: auto; }
      /* A column, so the Brands label can be ordered under Agencies and
         over B2B. In source it comes first (it is a grid item on desktop);
         stacked, it belongs where the hierarchy actually changes. */
      .nav-dd__cols { display: flex; flex-direction: column; min-width: 0; }
      .nav-dd__group:nth-of-type(1) { order: 0; }
      .nav-dd__parent               { order: 1; }
      .nav-dd__group:nth-of-type(2),
      .nav-dd__group:nth-of-type(3) { order: 2; }
      .nav-dd__group--shared        { order: 3; }
      .nav-dd__group + .nav-dd__group:not(.nav-dd__group--shared) { border-left: 0; padding-left: 0; }
      .nav-dd__group { border-top: 1px solid var(--border); }
      .nav-dd__group--shared { margin-top: 0; padding-top: 0; }
      .nav-dd__parent {
        margin: 0;
        padding: var(--s-3) 0 var(--s-1);
        border-bottom: 0;
      }
      .nav-dd__group-head {
        padding: var(--s-4) 0;
        cursor: pointer;
      }
      .nav-dd__group-name { font-size: 15px; color: var(--steel-100); }
      .nav-dd__group-head .nav-dd__sign { display: block; }
      .nav-dd__group[open] .nav-dd__group-head .nav-dd__sign::after { transform: rotate(0deg); opacity: 0; }
      .nav-dd__group-tag { padding: 0; margin: 0 0 var(--s-2); min-height: 0; }
      .nav-dd__group .nav-dd__list { padding-bottom: var(--s-2); }
      .nav-dd__list--row { display: block; }
      .nav-dd__list a { padding: var(--s-3) 0 var(--s-3) var(--s-2); font-size: 15px; }

      /* The one amber element stays the CTA, and it goes full width. */
      .nav .cta-secondary {
        margin-top: var(--space-sm);
        text-align: center;
        padding: calc(15px + 2px * var(--dial)) 22px;
        font-size: calc(15px + 2px * var(--dial));
      }

      body.nav-open { overflow: hidden; }
    }

    @media (max-width: 760px) and (prefers-reduced-motion: reduce) {
      .nav { transition: none; }
    }

    /* ---------- halos behind the CTAs ----------
       Each button carries .tx-halo: a contained cold field, resting low and
       lifting under the pointer. The pack's defaults are cut for a wordmark
       (34px x 18px reach); a button in a 72px bar needs less or the field
       leaves the bar, and a button on open ground can take more.

       Not on the ticker CTA. It is an opaque amber slab pinned inside a band
       that clips, so a field behind it is either hidden or cut off. */
    .nav .tx-halo {
      --tx-halo-pad-x: 28px;
      --tx-halo-pad-y: 11px;    /* 37px button + 22px = 59px, inside 72px */
      --tx-halo-low: 0.34;      /* present at rest, not only under the pointer */
      --tx-halo-high: 0.80;
    }

    .cta-row .tx-halo,
    .cform__actions .tx-halo,
    .foot-brand .tx-halo {
      --tx-halo-pad-x: 40px;
      --tx-halo-pad-y: 22px;
      --tx-halo-low: 0.30;
      --tx-halo-high: 0.68;
    }

    /* ---------- buttons (base classes live in tokens.css) ---------- */

    /* Pills. The one place the site goes fully round: a CTA is a thing you
       press, not a panel you read, and the softer form is what separates
       it from every ruled edge around it. */
    .cta, .cta-secondary {
      display: inline-block;
      padding: calc(13px + 4px * var(--dial)) calc(22px + 8px * var(--dial));
      font: inherit;
      font-size: calc(15px + 1px * var(--dial));
      font-weight: 600;
      text-decoration: none;
      cursor: pointer;
      border-radius: var(--r-pill);
      transition: background-color var(--dur-quick) var(--ease-hover), color var(--dur-quick) var(--ease-hover), border-color var(--dur-quick) var(--ease-hover);
    }

    /* The one rounded element on the site, and the reason it is allowed:
       it sits in the warm band, and a hard steel outline there read as a
       box nailed onto a soft surface. The fill is a warm TINT - sand at
       low strength - not the accent: "Book a call" beside it in the ticker
       is the viewport's one amber element, and a second would cancel it.
       The hover lifts the tint and the button a pixel; no shadow, per
       DESIGN.md, the halo behind it (tx-halo, tinted warm below) is what
       reads as the glow. */
    .nav .cta-secondary {
      padding: calc(9px + 2px * var(--dial)) calc(18px + 4px * var(--dial));
      font-size: calc(14px + 1px * var(--dial));
      border-radius: 10px;
      border-color: color-mix(in srgb, var(--sand) 34%, transparent);
      background: color-mix(in srgb, var(--sand) 10%, transparent);
      color: var(--steel-050);
      transition:
        background-color var(--dur-hover) var(--ease-hover),
        border-color var(--dur-hover) var(--ease-hover),
        color var(--dur-hover) var(--ease-hover),
        transform .25s var(--ease-enter);
    }
    .nav .cta-secondary:hover,
    .nav .cta-secondary:focus-visible {
      background: color-mix(in srgb, var(--sand) 18%, transparent);
      border-color: color-mix(in srgb, var(--sand) 58%, transparent);
      color: var(--white);
      transform: translateY(-1px);
    }
    /* Declared after the rule above on purpose: same specificity, so only
       source order decides, and placed earlier this silently lost. */
    @media (prefers-reduced-motion: reduce) {
      .nav .cta-secondary { transition: none; }
      .nav .cta-secondary:hover,
      .nav .cta-secondary:focus-visible { transform: none; }
    }

    /* Its halo, and the wordmark's, go warm with the band. */
    .site-header .tx-halo {
      --tx-halo-tint: color-mix(in srgb, var(--amber-burnt) 50%, var(--taupe-dark));
    }

    /* ---------- sections ---------- */

    section { padding: var(--space-xl) 0; }

    .hero {
      padding-top: var(--space-lg);
      padding-bottom: var(--space-xl);
    }

    /* The VSL. Separation is a lit top edge, not a drop shadow — the same
       move as .panel in tokens.css. Sits UNDER the claim now, so it opens
       with a margin above rather than below. */
    .hero__video {
      position: relative;
      aspect-ratio: 16 / 9;
      /* The video leads the hero now, so the gap goes under it, between the
         box and the headline - not above it, where it would only push the
         box down from the band. */
      margin-top: 0;
      margin-bottom: var(--space-lg);
      background: var(--bg-raised);
      border-top: 1px solid var(--border-lit);
      border-left: 1px solid var(--border);
      border-right: 1px solid var(--border);
      border-bottom: 1px solid var(--ink);
      border-radius: var(--r-lg);
      overflow: hidden;
    }

    .hero__video > iframe,
    .hero__video > video {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      border: 0;
    }

    /* Visible until a real embed replaces it. Loud on purpose. */
    .hero__video-placeholder {
      position: absolute;
      inset: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 10px;
      padding: 24px;
      text-align: center;
      color: var(--steel-700);
    }

    .hero__video-placeholder strong {
      color: var(--steel-100);
      font-size: calc(18px + 4px * var(--dial));
      letter-spacing: -0.02em;
    }

    /* ---------- the hero copy and the belt ----------
       One column of copy. The lane the clips run on is not in this grid:
       it is .hero__belt, positioned against .hero-ground by reel.js so it
       can start at the end of the headline's widest line and run to the
       true edge of the viewport, past the wrap's gutter. */
    .hero__grid {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
    }

    /* Wide enough that "Growth partner invested in your success." breaks
       as three lines at every size rather than four at the top end. */
    .hero h1 { max-width: 13ch; }
    /* The lead sat inside .hero__row, which carried the gap. With the lead
       back under the headline directly, the gap has to be its own. */
    .hero .lead { margin-top: var(--space-sm, 28px); }

    /* The copy block ends where the headline's widest line ends, so the
       belt's mouth is a clean edge and no card ever sits on the lead's
       last few characters. reel.js measures that line and writes the
       width to --copy-w; with no JS the lead keeps its 58ch. */
    .hero .lead { max-width: min(58ch, var(--copy-w, 58ch)); }

    /* The lane. Absolute against .hero-ground (position:relative and
       overflow:hidden, from .tx-thermal): a column from the headline's
       end to the viewport edge, from under the VSL box to the bottom of
       the hero, that the chain of cards drops through. Its own
       overflow:hidden is what lets a card slide out from under the box.
       top/left/width/height are all written by reel.js from measurement;
       the defaults here only exist so nothing flashes before it runs.
       z-index 1 puts it above the thermal ground (-1) and the hero's own
       flow content; pointer-events none so it never blocks a click. */
    .hero__belt {
      position: absolute;
      top: 0;
      left: 100%;
      width: 0;
      height: 0;
      overflow: hidden;
      z-index: 1;
      pointer-events: none;
    }

    /* With the VSL out of the hero the lane starts at the headline's top,
       and nothing covers the line the cards come in at, so they were cut
       flat across it. A short fade in, only while the box is absent: with
       it back, cards slide out from under it as before. */
    .hero-ground:not(:has(.hero__video)) .hero__belt {
      -webkit-mask-image: linear-gradient(to bottom, transparent, #000 72px);
              mask-image: linear-gradient(to bottom, transparent, #000 72px);
    }

    /* A column of the chain. reel.js positions it (left, width) and
       fills it with slots; the markup's two [data-belt-col] elements are
       the lists, read top to bottom. Below the two-column threshold the
       second is hidden and the first carries both lists interleaved. */
    .belt__col {
      position: absolute;
      top: 0;
      left: 0;
      width: var(--card-w, 220px);
      height: 100%;
    }
    .belt__col[hidden] { display: none; }

    /* A card - one slot in the chain, a <video> or an <img>. reel.js
       writes its y in translate3d and its opacity each frame, and sets
       aspect-ratio inline from the item's data-ar, so a square ad and a
       9:16 clip each keep their own shape rather than being cropped to a
       common one. The 9:16 here is only the value before reel.js runs.
       16px radius and a hairline outline is the site's panel edge
       without the lit top, which the media would paint over anyway. */
    .belt__card {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      aspect-ratio: 9 / 16;
      object-fit: cover;
      display: block;
      border-radius: var(--r-md);
      outline: 1px solid var(--border);
      outline-offset: -1px;
      background: var(--bg-raised);
      opacity: 0;
      transform: translate3d(0, 0, 0);
      will-change: transform, opacity;
    }

    /* Reduced motion: no drift, no autoplay. reel.js still measures the
       lane, then parks the first card at the top of it as a still (first
       frame only) and binds nothing else. */
    @media (prefers-reduced-motion: reduce) {
      .belt__col:first-child .belt__card:first-child { opacity: 1; }
    }

    /* Under the nav's own breakpoint the belt is gone and the copy takes
       the full width. The clips are never fetched here - reel.js checks
       the same query before it touches them - so a phone on data does
       not pay for a decoration it cannot see. */
    @media (max-width: 760px) {
      .hero__belt { display: none; }
      .hero .lead { max-width: 58ch; }
    }

    /* The rim ramp's shadow ends are --chrome-500, which at 150px turns the
       first word into a dark grey slab against the page. Same ramp, same
       lean, lifted two steps at the ends so the whole line stays legible
       and the specular band still reads as the bright part. Every page's
       h1 carries the class, so this is site-wide; the surface version in
       textures.css is untouched. */
    h1.tx-rim-text {
      --tx-rim-ramp: linear-gradient(
        100deg,
        var(--chrome-300) calc(0%  + var(--tx-rim-lean)),
        var(--chrome-100) calc(30% + var(--tx-rim-lean)),
        var(--chrome-050) calc(46% + var(--tx-rim-lean)),
        var(--chrome-050) calc(54% + var(--tx-rim-lean)),
        var(--chrome-100) calc(68% + var(--tx-rim-lean)),
        var(--chrome-300) 100%
      );
    }

    /* Lead left, actions right, on one line across the column. Below
       1000px they stack, lead first. */
    .hero__row {
      display: flex;
      align-items: flex-end;
      justify-content: space-between;
      gap: var(--space-sm) var(--space-md);
      margin-top: var(--space-md);
    }

    .hero__row .cta-row { flex: none; margin-top: 0; }

    @media (max-width: 1000px) {
      .hero__row { flex-direction: column; align-items: flex-start; }
    }

    .cta-row {
      display: flex;
      gap: calc(12px + 2px * var(--dial));
      flex-wrap: wrap;
      margin-top: var(--space-md);
    }

    .section-head { max-width: 62ch; margin-bottom: var(--space-md); }
    .section-head .lead { margin-top: var(--s-5); }

    /* Explicit columns, not auto-fit: both grids hold 3 or 6 cards, so a
       fixed 3 / 2 / 1 ladder never leaves an empty cell showing the
       hairline background as a solid block. */
    .grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 1px;
      background: var(--border);
      border: 1px solid var(--border);
    }

    @media (max-width: 900px) { .grid { grid-template-columns: repeat(2, 1fr); } }
    @media (max-width: 600px) { .grid { grid-template-columns: 1fr; } }

    /* Cards are separated by lit edges, not fills or shadows. */
    .grid .panel {
      border: none;
      border-top: 1px solid var(--border-lit);
      padding: var(--space-sm) 28px calc(var(--space-sm) + 8px);
    }

    .panel h3 + p { margin-top: 12px; }
    .panel p { font-size: 15px; color: var(--steel-500); }

    .index {
      display: block;
      font-size: 13px;
      font-weight: 600;
      color: var(--steel-900);
      margin-bottom: 14px;
    }

    /* ---------- stats ---------- */

    .stats {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
      gap: var(--space-sm);
      margin-top: var(--space-lg);
      padding-top: var(--space-md);
      border-top: 1px solid var(--border);
    }

    .stat .num {
      display: block;
      font-size: clamp(30px, 3.6vw, 44px);
      font-weight: 800;
      letter-spacing: -0.04em;
      line-height: 1;
      color: var(--steel-050);
    }

    .stat .label {
      display: block;
      margin-top: 10px;
      font-size: 14px;
      color: var(--text-muted);
    }

    /* ---------- punchline band: the three charts ----------
       Bars left, pie right, a rising line behind the headline. All three
       are static inline SVG in index.html; charts.js only animates them.

       Two fills, alternating. "one" is the site's white pulled a step
       toward the palette's magenta, as a top-to-bottom gradient. "two" is
       the gold ramp with a lit top edge: the same lit-metal move as the
       nav's warm button, and deliberately NOT a blurred glow.

       DESIGN.md exceptions recorded under "The punchline charts". */

    .punchline {
      position: relative;
      min-height: clamp(calc(460px + 60px * var(--dial)), calc(64vh + 16vh * var(--dial)), calc(720px + 180px * var(--dial)));
      display: grid;
      place-items: center;
      overflow: hidden;
      border-top: 1px solid var(--border);
      background: var(--bg);

      /* the two fills, as tokens the SVG reads through fill: var() */
      --ch-one-top:  color-mix(in srgb, var(--white, #E8F1F2) 90%, var(--magenta, #844C76));
      --ch-one-base: color-mix(in srgb, var(--steel-050, #D4E5EA) 84%, var(--magenta, #844C76));
      --ch-two-top:  var(--gold, #EDD997);
      --ch-two-mid:  var(--amber, #DFC38C);
      --ch-two-base: var(--amber-burnt, #AB804F);
    }

    .punchline__content {
      position: relative;
      z-index: 2;
      text-align: center;
      padding-block: var(--space-md);
    }

    .punchline :is(h1, h2) {
      max-width: 20ch;
      margin: 0 auto;
      color: var(--text-strong);
      text-wrap: balance;
    }
    .punchline :is(h1, h2) em { font-style: italic; color: var(--steel-050); }
    /* On work.html the band is the hero, so its headline is the page's h1
       and there is no band above it to draw a rule against. The h1 takes
       the h2's measure so the band is the home page's, chart for chart. */
    .punchline--hero { border-top: 0; }
    .punchline--hero h1 {
      font-size: clamp(calc(28px + 10px * var(--dial)), calc(4vw + 2vw * var(--dial)), calc(46px + 46px * var(--dial)));
      line-height: calc(1.02 - 0.06 * var(--dial));
    }

    /* The chart layer fills the band; each slot is placed absolutely. */
    .ch-layer {
      position: absolute;
      inset: 0;
      z-index: 1;
      pointer-events: none;
    }
    .ch-slot { position: absolute; }
    .ch { display: block; overflow: visible; }

    /* The line fills the band edge to edge. charts.js refits its viewBox
       to the band's aspect, so the drawing spans corner to edge
       uniformly. */
    .ch-slot--line { inset: 0; }
    .ch-slot--line .ch { width: 100%; height: 100%; }

    /* The bars sit clear of the corner: a step in from the gutter, and
       well up off the bottom edge. */
    .ch-slot--bars {
      left: calc(var(--gutter, 24px) + 3vw);
      bottom: clamp(var(--s-22), 16vh, calc(var(--s-27) + var(--s-8)));
      width: clamp(220px, 21vw, 300px);
    }
    .ch-slot--bars .ch { width: 100%; height: auto; }

    /* The pie sits in from the right edge and below the band's centre, so
       the line's endpoint at the top right has clear air around it and
       nothing is ever clipped by the viewport. */
    .ch-slot--pie {
      right: calc(var(--gutter, 24px) + 4vw);
      top: 60%;
      transform: translateY(-50%);
      width: clamp(170px, 16vw, 236px);
    }
    .ch-slot--pie .ch { width: 100%; height: auto; }

    /* ---- the three small motifs ----
       Placed in the negative space the big three leave: the heatmap in
       the top-left, the isotype grid top-right of the headline (under
       the line's arc, clear of both), the stacked bars centred under the
       headline. All three are smaller than the big three and sit at
       little over half strength, so they stay behind the copy. */
    .ch-slot--heat {
      left: calc(var(--gutter, 24px) + 7vw);
      top: clamp(var(--s-8), 9vh, var(--s-16));
      width: clamp(205px, 18.9vw, 281px);   /* 16 x 10 cells at the same cell size the 6 x 6 had */
      opacity: .62;
    }
    .ch-slot--stack {
      left: calc(50% + 90px);                /* a step right of centre, on the owner's eye */
      bottom: clamp(var(--s-10), 11vh, var(--s-18));
      transform: translateX(-50%);
      width: clamp(160px, 15vw, 220px);
      opacity: .6;
    }
    .ch-slot--people {
      left: calc(34% + 145px);               /* the top-centre gap, well right of it */
      top: clamp(var(--s-8), 8vh, var(--s-14));
      width: clamp(72px, 6.5vw, 96px);
      opacity: .6;
    }
    .ch-slot--heat .ch, .ch-slot--stack .ch, .ch-slot--people .ch { width: 100%; height: auto; }

    /* Two more, in what is left: the area chart in the bottom gap between
       the bar chart and the stacked bars, the horizontal bars in the
       bottom-right corner under the pie. Same rule: smaller, ~0.6
       strength, nothing under the copy. */
    .ch-slot--area {
      left: 29%;
      bottom: 0;                             /* on the band's floor */
      width: clamp(154px, 14vw, 202px);
      opacity: .6;
    }
    .ch-slot--hbars {
      right: calc(var(--gutter, 24px) + 5vw);
      bottom: clamp(var(--s-6), 6vh, var(--s-12));
      width: clamp(100px, 9.5vw, 140px);
      opacity: .6;
    }
    .ch-slot--area .ch, .ch-slot--hbars .ch { width: 100%; height: auto; }
    /* The area's hard left and right cuts read as pasted in, so its
       sides fade into the band. Mask, not gradient fill: the bands keep
       their tones. */
    .ch-slot--area .ch {
      -webkit-mask-image: linear-gradient(to right, transparent, var(--bg, #0C0C0C) 22%, var(--bg, #0C0C0C) 78%, transparent);
      mask-image: linear-gradient(to right, transparent, var(--bg, #0C0C0C) 22%, var(--bg, #0C0C0C) 78%, transparent);
    }

    /* ---- fills ----
       The gradients live once, in an SVG the layer carries; each shape
       references them by id. "one" is a vertical fade; "two" is the gold
       ramp with the brightest step at the top, and the lit edge is a
       1px stroke of the top colour on the bar's top or the slice's rim. */
    .ch-one { fill: url(#ch-grad-one); }
    .ch-two { fill: url(#ch-grad-two); stroke: var(--ch-two-top); stroke-width: 1; paint-order: stroke; }
    .ch-slice { stroke: var(--bg); stroke-width: 2; }            /* 2px surface gap between slices */
    .ch-slice.ch-two { stroke: var(--bg); }                       /* the rim stroke would fight the gap; the ramp carries the light */
    .ch-base { stroke: var(--border); stroke-width: 1; }
    /* the third tone the stacked bars need: the burnt gold, flat */
    .ch-three { fill: var(--ch-two-base); }
    .ch-cell { fill: var(--ch-two-mid); }                           /* strength is per cell, in the markup */
    .ch-person--on  { fill: var(--ch-two-mid); }
    .ch-person--off { fill: color-mix(in srgb, var(--ch-one-top) 22%, transparent); }
    .ch-hbar.ch-one { fill: var(--ch-one-top); }                    /* square, flat: the fill is the flat top colour */
    .ch-hbar.ch-two { fill: var(--ch-two-mid); stroke: none; }
    .ch-band--low { fill: var(--ch-two-mid); }
    .ch-band--mid { fill: color-mix(in srgb, var(--ch-one-top) 55%, transparent); }
    .ch-band--top { fill: color-mix(in srgb, var(--ch-two-mid) 30%, transparent); } /* the tone is in the fill, so the arrival tween can own opacity */

    .ch-path {
      fill: none;
      stroke: url(#ch-gold-line);
      stroke-width: 2.5;
      stroke-linejoin: round;
      stroke-linecap: round;
      vector-effect: non-scaling-stroke;
    }

    /* Bars grow from the baseline and slices sweep in once, on arrival;
       charts.js sets the start state and drives it, so nothing here hides
       anything. With reduced motion or no JS every mark is simply there. */
    .ch-bar { transform-box: fill-box; transform-origin: bottom; }

    @media (max-width: 900px) {
      /* The side charts do not fit beside a headline at this width, and
         stacked under it they read as three unrelated graphics. The line
         stays: it is the one that is part of the headline's frame. */
      .ch-slot--bars, .ch-slot--pie,
      .ch-slot--heat, .ch-slot--stack, .ch-slot--people,
      .ch-slot--area, .ch-slot--hbars { display: none; }
    }

    /* ---------- contact ---------- */

    .contact {
      border-top: 1px solid var(--border-lit);
      background: var(--bg-raised);
    }

    /* The thermal ground sits behind the hero and is clipped to it. */
    .hero-ground { position: relative; }

    /* The site's third and last metal moment. Thin on purpose: at this size
       it reads as a machined part, and anything taller starts reading as
       decoration. */
    .contact__bar {
      height: clamp(10px, 1.3vw, 15px);
      width: 100%;
      margin-bottom: var(--space-sm);
    }

    /* ---------- the contact column ----------
       Centred at every width, and it grows into the width it is given
       rather than sitting in a fixed column with dead space either side.

       This replaces a two-column grid (34a15df) that put the heading left
       and the form right from 1000px. Once the form moved inside
       .cform-wrap that rule placed the wrong element: the wrapper auto-
       flowed into the second column beside an empty first one, and on
       every inner page the form sat 600px to the right of a narrow heading.
       Under 1000px it was fine, which is why it read as "shifts right on
       wide screens".

       The answer to a wide screen is not a second column beside the form;
       it is a wider form. From 1000px the cap lifts to 1120px and the
       fields pair up - name | email, phone | website, revenue | heard, and
       the six pick rows in two rows of three - so the block fills what it
       is given and the textarea and the submit run the full width. */

    .contact .wrap { text-align: center; }
    .contact h2 { max-width: 20ch; margin-left: auto; margin-right: auto; }
    .contact .lead { margin-top: var(--space-sm); margin-left: auto; margin-right: auto; }

    .cform-wrap {
      margin: var(--space-md) auto 0;
      max-width: 800px;
      text-align: left;
    }
    .cform-wrap > .cform { margin-top: 0; max-width: none; }
    .cform__done, .cform__fail { margin-left: auto; margin-right: auto; }

    /* ---------- legal pages ----------
   Privacy, terms and cookies. One shared block rather than a <style> in each
   of the three, which is how the case studies do it - they differ from each
   other, these do not.

   Measure is the whole job here. Body copy elsewhere on the site is short
   enough that .lead's 62ch cap carries it; a policy is the one place someone
   reads several hundred words in a row, so the column is capped at 68ch and
   the headings get real space above them to give the eye somewhere to rest. */

.legal {
  padding: var(--space-lg) 0 var(--space-xl);
  max-width: 68ch;
}

.legal__eyebrow { margin: 0 0 var(--s-3); }

.legal h1 { margin: 0 0 var(--s-3); }

.legal__date {
  margin: 0 0 var(--space-md);
  font-size: 14px;
  color: var(--steel-700);
}

.legal h2 {
  margin: var(--space-md) 0 14px;
  font-size: clamp(20px, 2.4vw, 26px);
}

/* The summary is a real panel, so the reader can take the gist without the
   rest. Reuses .panel from tokens.css: lit top edge, ink bottom, no shadow.

   Declared AFTER .legal h2 on purpose. Both selectors are one class and one
   type, so they carry identical specificity and source order is the only
   tie-break - written the other way round, the general rule won and the
   summary heading picked up a --space-md top margin it should not have.

   margin-bottom is --space-sm, not --space-lg: the next h2 brings its own
   --space-md above it, and the two stacked read as a hole in the page. */
.legal__summary {
  padding: clamp(24px, 2.6vw, 36px);
  margin-bottom: var(--space-sm);
  border-radius: var(--r-md);
}

.legal__summary h2 {
  margin: 0 0 10px;
  font-size: clamp(17px, 1.9vw, 20px);
  color: var(--steel-050);
}

.legal__summary p { margin: 0; color: var(--steel-100); }

.legal h3 {
  margin: var(--space-sm) 0 10px;
  font-size: clamp(15px, 1.6vw, 17px);
  color: var(--steel-050);
}

.legal p,
.legal li {
  color: var(--steel-300);
  line-height: 1.75;
}

.legal p { margin: 0 0 var(--s-4); }

.legal ul {
  margin: 0 0 var(--s-4);
  padding-left: var(--s-5);
}

.legal li { margin-bottom: 10px; }

.legal strong { color: var(--steel-050); font-weight: 600; }

.legal code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .9em;
  padding: 2px 6px;
  color: var(--steel-050);
  background: var(--charcoal);
  border: 1px solid var(--border);
}

.legal a {
  color: var(--steel-100);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.legal a:hover { color: var(--white); }

/* ---------- inline enquiry form ----------
       Fields are --charcoal, which tokens.css nominates for exactly this
       job. Separation is the rim-light move the whole site uses - lit top
       edge, --border sides, --ink bottom - so an input reads as the same
       kind of surface as .panel and .hero__video rather than as a widget
       borrowed from somewhere else. No radius, no inset shadow.

       Labels sit above their field in sentence case. DESIGN.md rules out
       tracked-out all-caps eyebrows, and a form is where that habit
       usually creeps back in.

       Focus needs nothing here: the global :focus-visible rule already
       puts an amber outline on input and textarea. */

    /* One column with a gap rather than margins on each field. The margin
       version left the textarea's label crowding the row above it, because
       the grid children had their margin zeroed and the grid itself carried
       none - the classic collapse this avoids entirely. */
    /* Matched to badmarketing.com's work-with form, measured: one column,
       every field full width, label above at ~21px, fields ~60px tall with a
       1px light border and a 12px radius on a fill darker than the ground,
       textarea ~270px, pick rows stacked full width at 2px border. Colours
       are tokens - their pure white is our --steel-050, their red pill is
       our amber - and the type is the site's stack; everything else is a
       like-for-like. */
    .cform {
      margin-top: var(--space-md);
      max-width: calc(760px + 100px * var(--dial));
      display: flex;
      flex-direction: column;
      gap: var(--s-8);
    }

    .cform__grid {
      display: grid;
      grid-template-columns: 1fr;
      gap: var(--s-8);
    }

    .cform__field {
      display: flex;
      flex-direction: column;
      gap: var(--s-2);
      margin: 0;
    }

    .cform__field label {
      font-size: clamp(17px, 1.5vw, 21px);
      color: var(--steel-050);
    }

    .cform__field input,
    .cform__field textarea {
      font: inherit;
      font-size: clamp(20px, 2.2vw, 32px);
      width: 100%;
      min-height: 61px;
      padding: var(--s-2) var(--s-4);
      color: var(--white);
      background: var(--bg);
      border: 1px solid var(--steel-050);
      border-radius: 12px;
      transition: border-color var(--dur-quick) var(--ease-hover), background-color var(--dur-quick) var(--ease-hover);
    }

    /* The spam trap. Off screen rather than display:none - a bot that
       reads the DOM skips anything hidden the obvious way, and this needs
       to look like a field worth filling. Not clipped to 0x0 either: some
       bots treat a zero-size input as hidden too. aria-hidden and
       tabindex="-1" on the markup keep it away from real visitors. */
    .cform__trap {
      position: absolute;
      left: -9999px;
      top: auto;
      width: 1px;
      height: 1px;
      overflow: hidden;
    }

    /* Hint text under a field, tied to it by aria-describedby so a screen
       reader reads the rule with the label rather than after the mistake. */
    .cform__field .cform__hint {
      display: block;
      margin-top: var(--s-1);
      font-size: var(--fs--1);
      color: var(--text-muted);
    }

    /* 16px floor on the pick rows. Anything under it makes iOS zoom the
       page when the row is tapped, and the zoom does not undo itself. */
    .cform__row { font-size: max(16px, var(--fs--1)); }

    .cform__field textarea { resize: vertical; min-height: 270px; padding: 14px 16px; }

    .cform__field input::placeholder,
    .cform__field textarea::placeholder { color: var(--steel-700); }

    .cform__field input:hover,
    .cform__field textarea:hover { border-color: var(--white); }
    .cform__field input:focus-visible,
    .cform__field textarea:focus-visible { outline: 2px solid var(--steel-100); outline-offset: 3px; }

    .cform__actions {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 18px;
      margin-top: 10px;
      text-align: center;
    }
    .cform__actions .cta {
      border-radius: 12px;
      min-width: 232px;
      min-height: 47px;
      padding: 10px 24px;
      font-size: 18px;
      font-weight: 700;
    }

    /* Submission states. Theirs exist too - "Thank you! Your submission has
       been received!" / "Oops! Something went wrong" - and both are set in
       #f4f4f4 on #f4f4f4, so neither can be read. Ours are hidden until the
       endpoint script shows one, and legible when it does. */
    .cform__done,
    .cform__fail {
      max-width: 800px;
      margin-top: var(--space-sm);
      padding: 22px 24px;
      border-radius: 12px;
      font-size: clamp(17px, 1.5vw, 20px);
      text-align: center;
    }
    .cform__done { color: var(--white); background: var(--bg); border: 1px solid var(--steel-050); }
    .cform__fail { color: var(--white); background: var(--bg); border: 1px solid var(--magenta-hot); }

    .cform__note {
      max-width: 56ch;
      font-size: 14px;
      line-height: 1.6;
      /* --steel-500, not --steel-700. On the homepage the contact section is
         --bg-raised, where steel-700 measures 4.05 and fails WCAG AA; on the
         inner pages the section is transparent and it scrapes past on --bg at
         4.51. One token that passes on both grounds beats one that depends on
         which page it landed on. */
      color: var(--steel-500);
    }

    .cform__note a { color: var(--steel-100); text-decoration: underline; }
    .cform__note a:hover { color: var(--white); }

    @media (max-width: 640px) {
      .cform__grid { grid-template-columns: 1fr; }
    }


    /* ---------- the pick rows ----------
       The device from badmarketing.com: full-width rows you tap to select,
       several at once. Real checkboxes underneath - visually hidden, never
       removed - so the keyboard and screen readers get the native control
       and the pointer gets the row. */

    .cform__pick {
      margin: 0;
      padding: 0;
      border: 0;
      min-width: 0;
    }

    .cform__pick legend {
      padding: 0;
      margin-bottom: 12px;
      font-size: clamp(17px, 1.5vw, 21px);
      color: var(--steel-050);
    }
    .cform__pick legend .cform__hint { display: block; margin: 4px 0 0; font-size: 13px; font-style: italic; }

    /* --steel-500, not the --steel-900 the ramp suggests for a hint: on the
       homepage this section is --bg-raised, where steel-900 is 2.80:1 and
       steel-700 is 4.05 - both fail AA. steel-500 is 5.75 there. */
    .cform__hint,
    .cform__opt {
      color: var(--steel-500);
      font-size: 13px;
      margin-left: 6px;
    }

    .cform__pick-grid {
      display: grid;
      grid-template-columns: 1fr;
      gap: 5px;
    }

    .cform__row {
      position: relative;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 62px;
      padding: 8px 48px;
      text-align: center;
      font-size: clamp(20px, 2.2vw, 32px);
      font-weight: 500;
      color: var(--steel-050);
      background: transparent;
      border: 2px solid var(--steel-050);
      border-radius: 12px;
      cursor: pointer;
      -webkit-user-select: none;
      user-select: none;
      transition: background-color var(--dur-quick) var(--ease-hover), color var(--dur-quick) var(--ease-hover), border-color var(--dur-quick) var(--ease-hover);
    }

    /* The checkbox: off the visual layer, still in the tab order, still
       announced. clip, not display:none - display:none removes it from
       the accessibility tree and the keyboard. */
    .cform__row input {
      position: absolute;
      width: 1px; height: 1px;
      margin: -1px; padding: 0;
      overflow: hidden;
      clip: rect(0 0 0 0);
      clip-path: inset(50%);
      white-space: nowrap;
      border: 0;
    }

    /* The tick. A drawn 12px mark at the trailing edge, hidden until
       checked, so a selected row has two signals - fill and mark - not a
       colour change alone. */
    .cform__row::after {
      content: "";
      position: absolute;
      right: 20px;
      top: 50%;
      width: 14px;
      height: 8px;
      border-left: 2px solid var(--amber);
      border-bottom: 2px solid var(--amber);
      transform: translateY(-60%) rotate(-45deg) scale(0.6);
      opacity: 0;
      transition: opacity var(--dur-quick) var(--ease-hover), transform var(--dur-quick) var(--ease-hover);
    }

    .cform__row:hover { border-color: var(--white); color: var(--white); }

    .cform__row:has(input:checked) {
      color: var(--white);
      background: var(--bg);
      border-color: var(--white);
    }
    .cform__row:has(input:checked)::after { opacity: 1; transform: translateY(-60%) rotate(-45deg) scale(1); }

    .cform__row:has(input:focus-visible) {
      outline: 2px solid var(--steel-100);
      outline-offset: 2px;
    }

    /* No :has() - a browser old enough not to have it gets the native
       checkbox back, visible, in the row, and everything still works. */
    @supports not selector(:has(*)) {
      .cform__row input {
        position: static; width: auto; height: auto; margin: 0 10px 0 0;
        clip: auto; clip-path: none; overflow: visible;
      }
      .cform__row::after { display: none; }
    }

    @media (max-width: 640px) {
      .cform__pick-grid { grid-template-columns: 1fr; }
    }

    /* Wide screens: the form widens and pairs its fields instead of
       sitting in a fixed column with dead space either side. Placed after
       every base rule it overrides - they are single-column by default and
       win on source order otherwise. */
    @media (min-width: 1000px) {
      /* No cap of its own from here: the form runs edge to edge of .wrap,
         which already stops at --wrap. On a 1440 screen that is 1336px;
         on anything wider, 1360. Side to side, as asked. */
      .cform-wrap { max-width: none; }
      .cform__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: clamp(20px, 2.4vw, 32px); }
      .cform__pick-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: clamp(10px, 1.4vw, 16px); }
      /* Three across, the longest row is ~350px at 1000px; the single-column
         text size wrapped "Tracking and attribution" to two lines. */
      .cform__row { font-size: clamp(17px, 1.6vw, 24px); padding-left: 36px; padding-right: 36px; }
      .cform__done, .cform__fail { max-width: none; }
    }


    /* 16px is a hard floor on touch, not a preference. iOS Safari zooms the
       whole page in when a field with a computed font-size under 16px is
       focused, and it does not zoom back out - so tapping "Your name" on an
       iPhone would leave the visitor scrolled sideways inside a magnified
       page for the rest of the form. 15px everywhere else keeps the field
       text matched to the labels beside it.

       Keyed to pointer:coarse rather than a width, because the zoom is a
       property of the input method: a touch tablet in landscape is well past
       any mobile breakpoint and still does it. Same reasoning the texture
       pack uses for its (hover: none) branch. */
    @media (pointer: coarse) {
      .cform__field input,
      .cform__field textarea { font-size: 16px; }
    }

    @media (prefers-reduced-motion: reduce) {
      .cform__field input,
      .cform__field textarea,
      .cform__row,
      .cform__row::after { transition: none; }
    }

    /* ---------- footer ----------
       Modeled on the vaynermedia.com footer: a full-bleed social strip
       across the top, then a block of two link columns with the wordmark
       and a single action right-aligned opposite them, and the copyright
       line underneath.

       Two deliberate departures from the reference:

       1. Vayner's social strip is a saturated accent field. Ours is too,
          but in --petrol rather than an accent colour: a strip of amber
          that size would spend the page's one warm accent on a row of
          icons, and the contact CTA above the footer is what needs it.
          Full reasoning sits on .foot-social below.
       2. Vayner's left column is partner badges — Google, Meta, TikTok,
          Snap. We don't hold those, so that slot is a site nav column. If
          real partner badges ever land, this is where they go. */

    .site-footer {
      border-top: 1px solid var(--border);
    }

    /* Full-bleed by design: it should read as a band across the page, not
       as a row sitting inside the content column. */
    /* A real band, not a tint. Modelled on vaynermedia.com's footer social
       strip, which works because it is a hard full-bleed colour change that
       separates the page from the footer in one move. Ours was --bg-raised
       on --bg: a 12-point step nobody sees, so the strip read as part of the
       footer rather than as the seam above it.

       --petrol, not the obvious --amber. DESIGN.md is explicit that amber
       marks the primary action and nothing else, one per viewport, and the
       footer's own CTA is already the action here - a 50px amber bar beside
       it would cancel it. --paper was the other candidate and is deliberately
       not used either: it is the only light surface on the site and the
       full-colour platform logos are the reason it exists. --petrol is the
       token for "cool depth behind steel elements", it is cold like the rest
       of the page, and it costs nothing from the warm budget.

       Rim light holds: lit top edge, ink bottom, mixed in-family so the
       highlight belongs to the petrol rather than sitting on it as grey. */
    .foot-social {
      background: var(--petrol);
      border-top: 1px solid color-mix(in srgb, var(--petrol) 58%, var(--steel-100));
      border-bottom: 1px solid var(--ink);
    }

    .foot-social__list {
      max-width: var(--wrap);
      margin: 0 auto;
      padding: calc(14px + 4px * var(--dial)) var(--gutter);
      list-style: none;
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: clamp(18px, 5vw, 56px);
    }

    /* --steel-050 rather than --steel-500: on petrol the muted value sank
       into the band and the icons stopped reading as marks. */
    .foot-social__link {
      display: block;
      padding: 6px;   /* touch target only — reads as no padding at all */
      color: var(--steel-050);
      transition: color var(--dur-quick) var(--ease-hover);
    }

    .foot-social__link:hover,
    .foot-social__link:focus-visible { color: var(--white); }

    .foot-social__link svg {
      display: block;
      width: calc(22px + 4px * var(--dial));
      height: calc(22px + 4px * var(--dial));
      fill: currentColor;
    }

    /* Link columns left, brand block right, copyright spanning underneath.
       .wrap supplies the horizontal gutter; this only adds the vertical. */
    .foot {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, auto);
      gap: var(--space-sm) clamp(24px, 5vw, 72px);
      align-items: start;
      padding-top: var(--space-lg);
      padding-bottom: var(--space-sm);
    }

    .foot-col {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      /* gap is 0 because .foot-link now carries its own vertical padding to
         reach a 44px tap target. Head margin (4px) + the first link's top
         padding (10px) reproduces the 14px that the old 4px + 10px gap gave,
         so the head sits exactly where it did. */
      gap: 0;
    }

    /* Overrides the global h1-h3 rule: at 13px the display tracking of
       -0.035em closes the letters up into a smudge. */
    .foot-col__head {
      margin: 0 0 4px;
      font-size: calc(13px + 1px * var(--dial));
      font-weight: 600;
      line-height: 1;
      letter-spacing: 0;
      color: var(--steel-700);
    }

    /* Google's mobile usability guidance wants a 44-48px tap target and
       WCAG 2.5.8 sets 24px as the hard floor. These measured 25px tall —
       one pixel above an outright failure — because an inline <a> at 15px
       is only as tall as its line box. Padding makes the target real.

       Padding rather than a stretched ::after pseudo-element: these sit
       10px apart in a column, so a 44px overlay on each would overlap its
       neighbours and cause mis-taps, which is worse than a small target. */
    .foot-link {
      display: block;
      padding-block: 10px;          /* 25-27px line box + 20px = 45-47px */
      font-size: calc(15px + 2px * var(--dial));
      text-decoration: none;
      color: var(--steel-300);
      transition: color var(--dur-quick) var(--ease-hover);
    }

    .foot-link:hover,
    .foot-link:focus-visible { color: var(--white); }

    .foot-brand {
      display: flex;
      flex-direction: column;
      align-items: flex-end;
      gap: 20px;
      text-align: right;
    }

    /* width/height in the markup are the file's real pixels, so the box is
       the right shape before the image lands. */
    .foot-brand__logo {
      display: block;
      width: clamp(calc(170px + 30px * var(--dial)), calc(20vw + 2vw * var(--dial)), calc(240px + 80px * var(--dial)));
      height: auto;
    }

    .foot-copy {
      grid-column: 1 / -1;
      margin: 0;
      padding-top: var(--space-sm);
      border-top: 1px solid var(--border);
      font-size: 13px;
      line-height: 1.7;
      text-align: right;
      color: var(--steel-700);
    }

    /* ---- legal links, on the copyright line ----
       Modelled on inboundmedia.co, which runs "© INBOUND 2026 | PRIVACY
       POLICY" as one line of small print. theinfiniteagency.com goes further
       and carries no legal links at all, which is not an option here.

       So: the copyright's own 13px, the copyright's own --steel-700, no
       underline, no column heading, no tap-target padding. They read as part
       of the small print rather than as somewhere to go, which is exactly
       what small print is for.

       WHAT WAS DELIBERATELY NOT DONE, and why the brief stopped here:
       they are not styled to look unclickable. --steel-700 on --bg measures
       4.51:1, just over the 4.5 floor AA sets for small text; the hover lift
       stays; and the global :focus-visible outline still finds them. Quiet
       is a style choice. Inconspicuous is a legal one, and it runs straight
       into the reason these pages exist - CCPA wants a "clear and
       conspicuous" privacy link, courts decline to enforce terms nobody
       could reasonably find, and Google Ads and Meta both check for a
       reachable policy before they will run traffic. A link too dead to
       click is a link that costs you the ad account.

       A hairline divider rather than a middle dot: DESIGN.md rules those
       out, and a rule is this site's own way of separating things anyway. */
    .foot-legal {
      display: inline-flex;
      flex-wrap: wrap;
      gap: 0 14px;
      margin-left: 14px;
      padding-left: 14px;
      border-left: 1px solid var(--slate);
    }

    .foot-legal a {
      color: inherit;              /* same value as the copyright beside it */
      text-decoration: none;
      transition: color var(--dur-quick) var(--ease-hover);
    }

    .foot-legal a:hover,
    .foot-legal a:focus-visible { color: var(--steel-050); }

    /* Once the copyright wraps, a left rule on a new line reads as a stray
       mark. Below that it sits under the line instead, still quiet. */
    @media (max-width: 560px) {
      .foot-legal {
        display: flex;
        margin-left: 0;
        padding-left: 0;
        border-left: 0;
        margin-top: 8px;
        justify-content: flex-end;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      .foot-legal a { transition: none; }
    }

    /* Brand block leads once the three columns stop fitting — the wordmark
       is the thing worth seeing first on a phone, not the nav list. */
    @media (max-width: 860px) {
      .foot { grid-template-columns: repeat(2, minmax(0, 1fr)); }

      .foot-brand {
        order: -1;
        grid-column: 1 / -1;
        align-items: flex-start;
        text-align: left;
      }

      .foot-copy { text-align: left; }
    }

    @media (max-width: 460px) {
      .foot { grid-template-columns: minmax(0, 1fr); }
    }

    @media (prefers-reduced-motion: reduce) {
      .foot-social__link,
      .foot-link { transition: none; }
    }

    /* ---------- the wordmark's face, for the word band ----------
       Bebas Neue: the tall condensed grotesque the SILVER BULLET wordmark is
       set in. The site's first and only web font, and it is self-hosted on
       purpose - no fonts.googleapis.com origin, nothing new for the privacy
       policy, no CDN in the path of the first paint. OFL licence. Latin
       subset only, 8.6KB. Used ONLY by the word band; every other line on
       the site stays on the system stack the way it was.

       font-display: swap, and the file is preloaded from <head>, so it is
       almost always ready before the band paints. If it is not, the band
       paints in the fallback and swaps - motion.js re-measures the loop on
       document.fonts.ready so the wrap does not go stale. */
    @font-face {
      font-family: "Bebas Neue";
      font-style: normal;
      font-weight: 400;
      font-display: swap;
      src: url("Assets/fonts/bebas-neue-latin.woff2") format("woff2");
    }

    /* ---------- the word band ----------
       Two rows of big hollow words under the nav, a lit subset among them.
       Modelled on thearmcandy.com's client-partners strip, which runs three
       rows of 48px outlined industries with every other one lit neon pink.

       What is kept from the reference: hollow letters (a stroke and no
       fill), the "//" between words, a fixed lit subset, rows that slide
       at different speeds. What is changed: two rows not three, cold white
       not pink, and the words are what this company is - identity
       resolution, data architecture, paid on performance - mixed with the
       lines the old strip carried.

       THE LIT WORDS CARRY A GLOW, AND THAT IS A KNOWING EXCEPTION. DESIGN.md
       rules out text-shadow bloom, and this is text-shadow bloom. It is
       here because the brief was explicitly the neon on the reference, as
       an experiment, and a neon with no bloom is not a neon. It is held
       cold (white over --steel-050, never warm), kept to the lit words in
       this one band, and it does not pulse - the lit set is fixed, and the
       "flash" is a lit word passing a hollow one. If the experiment is
       kept, this paragraph should move to DESIGN.md as the recorded
       exception; if not, delete .is-lit's text-shadow and it is back on
       the sheet.

       Structure per row mirrors the old single strip exactly - row (the
       clip and the grab area) > travel (intro only) > track (the loop) -
       so the intro ride-out and motion.js both work on it untouched. */

    .ticker {
      position: relative;
      padding: 8px 0 12px;
      background: var(--bg-warm);
    }

    /* The band's lower edge is a fade, not a line: the warm dark dissolves
       into the page's cold dark over 28px. Below the strip, so it never
       overlaps the words. */
    .ticker::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      top: 100%;
      height: 28px;
      pointer-events: none;
      background: linear-gradient(180deg, var(--bg-warm), transparent);
    }

    /* Each row clips its own loop and is its own grab area. Words fade in
       on the left and out on the right rather than being cut. The mask does
       not touch pointer events, so drag is unaffected. */
    .ticker__row {
      position: relative;
      overflow: hidden;
      -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
      mask-image: linear-gradient(90deg, transparent 0, #000 6%, #000 94%, transparent 100%);
    }

    .ticker__row + .ticker__row { margin-top: -4px; }

    /* Intro-only layer: rides out with the bullet and returns. Nothing here
       at rest; body.is-intro adds the animation. See the intro section. */
    .ticker__travel { will-change: transform; }

    /* The no-JS fallback loop. motion.js reads the duration, cancels this,
       and drives the same motion itself so the band can be grabbed; with
       JS off the rows simply run on these. The speeds in the markup are
       what JS uses; these approximate them for the fallback. */
    .ticker__track {
      display: flex;
      width: max-content;
      animation: marquee-left var(--ticker-rate) linear infinite;
    }
    .ticker__row--b .ticker__track {
      animation-duration: var(--ticker-rate-b);
      /* The second row starts part way round so the two lit patterns are
         never in phase on first paint. */
      animation-delay: calc(var(--ticker-rate-b) * -0.4);
    }

    /* Two identical sets side by side, no gap, so translating the track by
       exactly -50% lands the second set where the first began. Shared by
       every band on the site. Put a gap on the track and this breaks. */
    .ticker__set {
      display: flex;
      flex: none;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    /* Hollow words. A stroke and no fill is rim light in its purest form:
       the highlight IS the contour. Uppercase because a marquee of set
       phrases is a display band, not an eyebrow over a heading. */
    .ticker__set li {
      position: relative;
      flex: none;
      padding: 0 clamp(10px, 1.4vw, 22px) 0 0;
      /* Bebas is one weight, so 400 - asking for 600 would fake-bold it and
         thicken the hollow stroke. Condensed, so it runs larger than the
         system face did at the same footprint. The fallbacks are the
         narrowest faces a Mac or Windows box has, so a pre-swap frame is
         at least the right shape. */
      font-family: "Bebas Neue", Impact, "Arial Narrow", "Helvetica Neue Condensed", sans-serif;
      font-size: clamp(28px, 3.4vw, 50px);
      line-height: 1.1;
      font-weight: 400;
      letter-spacing: 0.045em;
      text-transform: uppercase;
      white-space: nowrap;
      color: transparent;
      -webkit-text-fill-color: transparent;
      -webkit-text-stroke: 1px var(--steel-700);
    }

    /* The separator, drawn in the same hollow stroke so it belongs to the
       row rather than punctuating it. */
    .ticker__set li::before {
      content: "//";
      margin-right: clamp(10px, 1.4vw, 22px);
      -webkit-text-stroke: 1px var(--steel-900);
      letter-spacing: -0.06em;
    }

    /* The lit subset. Filled cold white with the stroke matched, plus the
       bloom - see the note at the top of this block for why it is here. */
    .ticker__set li.is-lit {
      -webkit-text-fill-color: var(--white);
      -webkit-text-stroke: 1px var(--white);
      text-shadow:
        0 0 2px  var(--white),
        0 0 10px color-mix(in srgb, var(--steel-050) 62%, transparent),
        0 0 24px color-mix(in srgb, var(--steel-050) 28%, transparent);
    }

    @keyframes marquee-left {
      from { transform: translateX(0); }
      to   { transform: translateX(-50%); }
    }

    /* ---------- the stack: work pins, clients covers it ----------
       The pin is CSS. .work is position:sticky with a NEGATIVE top, so it
       scrolls normally until that much of it has passed the top of the
       viewport, then holds there. The value is set by stack.js as
       --stack-pin: a fifth of the section's own height, or, if the section
       is taller than the viewport, enough that the bottom of it has been
       seen before it pins (a percentage top would measure the viewport,
       not the section, which is why it is a pixel value from JS). The
       fallback here is a fifth of the viewport, for the moment before the
       script runs.

       .clients is the cover: relative, above .work, and opaque. It has to
       be opaque - it is sliding over a section that is still painting,
       marquee and all - so it takes --bg. That also means the nightfield
       backdrop no longer shows through this one section; it never showed
       through much of it, and a see-through cover is not a cover.

       No overflow anywhere on this chain. overflow on any ancestor turns
       it into the sticky element's scrollport and the pin silently stops
       working; the header's pin is the proof the chain is clean today.

       Push-back polish - the pinned section scaling down and dimming as
       it is covered - is stack.js writing --stack-progress (0-1) each
       frame. The transform and filter below read it. Under reduced motion
       they are dropped and only the pin and the cover remain: the overlap
       is still legible, nothing is pushed. Under 768px the pin is off and
       the two sections scroll like any other pair. */

    .stack { position: relative; }

    .stack .work {
      position: sticky;
      top: var(--stack-pin, -20vh);
      z-index: 1;
      --stack-progress: 0;
      transform: scale(calc(1 - var(--stack-progress) * 0.05));
      filter: brightness(calc(1 - var(--stack-progress) * 0.4));
      transform-origin: 50% 50%;
      will-change: transform, filter;
    }

    .stack .clients {
      position: relative;
      z-index: 2;
      background: var(--bg);
    }

    /* The incoming edge. A dark fringe above the cover section, drawn as a
       gradient rather than a box-shadow (which DESIGN.md rules out for
       depth) - but this is not faked depth, the section really is on top,
       and on black over black the seam needs SOMETHING or the cover reads
       as the page simply continuing. The lit hairline the section already
       carries is the rim; this is the fall-off under it. */
    .stack .clients::before {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      bottom: 100%;
      height: 56px;
      pointer-events: none;
      background: linear-gradient(to top,
        color-mix(in srgb, var(--ink) 72%, transparent),
        transparent);
    }

    @media (prefers-reduced-motion: reduce) {
      .stack .work {
        transform: none;
        filter: none;
        will-change: auto;
      }
    }

    @media (max-width: 767px) {
      .stack .work {
        position: static;
        transform: none;
        filter: none;
        will-change: auto;
      }
      .stack .clients::before { display: none; }
    }

    /* ---------- a glimpse at our work ---------- */

    .work { padding: var(--space-xl) 0; }

    /* Heading left, the case-study sentence right, both on the baseline of
       the row. One line across the page instead of two stacked in the left
       third of it. Stacks below 1000px. */
    .work__head {
      margin-bottom: var(--space-md);
      display: flex;
      align-items: flex-end;
      justify-content: space-between;
      gap: var(--space-sm) var(--space-md);
    }
    .work__head h2 { flex: none; max-width: 10ch; }
    .work__head .work__cases { max-width: 44ch; }

    @media (max-width: 1000px) {
      .work__head { flex-direction: column; align-items: flex-start; }
    }

    /* ---------- 404 ----------
       Reuses the page furniture rather than inventing a layout: same wrap,
       same type scale, same single amber CTA. The list is the whole point of
       the page, so it gets the lit-edge separation the panels use instead of
       being a bare <ul>. */
    .notfound { padding-top: var(--space-lg); padding-bottom: var(--space-xl); }

    .notfound__code {
      font-size: 13px;
      letter-spacing: 0.16em;
      color: var(--steel-900);
      margin: 0 0 12px;
    }

    .notfound h1 { max-width: 18ch; }
    .notfound .lead { margin-top: var(--space-sm); }

    .notfound__list {
      list-style: none;
      margin: var(--space-md) 0 0;
      padding: 0;
      display: grid;
      gap: 1px;
      background: var(--border);
      border-top: 1px solid var(--border-lit);
      border-bottom: 1px solid var(--ink);
      border-radius: var(--r-md);
      overflow: hidden;
    }

    .notfound__list li {
      background: var(--bg);
      padding: calc(18px + 4px * var(--dial)) calc(20px + 4px * var(--dial));
      display: grid;
      gap: 4px;
    }

    .notfound__list a {
      color: var(--steel-050);
      font-size: calc(17px + 4px * var(--dial));
      font-weight: 600;
      letter-spacing: -0.01em;
      text-decoration: none;
    }
    .notfound__list li:hover a { color: var(--white); }
    .notfound__list a:focus-visible { outline: 2px solid var(--steel-100); outline-offset: 3px; }
    .notfound__list span { color: var(--steel-500); font-size: calc(14px + 2px * var(--dial)); }


    /* Inline links in body copy. The site sets `a { color: inherit }`
       globally, which is right for cards and nav items wrapped in an anchor
       but leaves a genuine inline link indistinguishable from the sentence
       around it. These get a lit underline instead of a colour change: the
       rim-light move, not a second accent, so the amber stays rationed to
       the CTA. */
    .work__cases { margin-top: 0; }

    .work__cases a {
      color: var(--steel-050);
      text-decoration: none;
      border-bottom: 1px solid var(--border-lit);
      padding-bottom: 1px;
      transition: color var(--dur-base) var(--ease-metal),
                  border-color var(--dur-base) var(--ease-metal);
    }
    .work__cases a:hover { color: var(--white); border-color: var(--steel-500); }
    .work__cases a:focus-visible { outline: 2px solid var(--steel-100); outline-offset: 3px; }

    .conveyor,
    .logo-band { overflow: hidden; }

    .conveyor__track,
    .logo-band__track {
      display: flex;
      width: max-content;
    }

    /* Promoted for the life of the page — see the note on .ticker__travel
       above. Gating this on .is-onstage was the cause of the ghosting. */
    .conveyor__track,
    .logo-band__track,
    .ticker__track { will-change: transform; }

    .conveyor__track  { animation: marquee-left var(--conveyor-rate) linear infinite; }
    .logo-band__track { animation: marquee-left var(--logo-rate)     linear infinite; }

    .conveyor__set,
    .logo-band__set {
      display: flex;
      margin: 0;
      padding: 0;
      list-style: none;
    }

    /* Four in view. Width is derived from the viewport rather than set in
       px, so the count stays exactly right at every width instead of
       drifting to three-and-a-bit. No flex `gap` anywhere — the spacing is
       margin on the item, because a gap on the track would break the -50%
       loop. */
    /* Three in view, not four. At four the photographs were 330px wide on
       a 1440 screen, which is thumbnail territory; three puts them at
       ~460px and they start to read as work rather than as a strip. */
    .conveyor {
      /* Fractional in between: 3.61 slots in view at 0.39 is fine, the
         width formula below takes any number. */
      --cols: calc(4 - 1 * var(--dial));
      --slot-gap: clamp(calc(12px + 2px * var(--dial)), calc(1.6vw + 0.2vw * var(--dial)), calc(24px + 4px * var(--dial)));
    }

    /* One height, two shapes. The photo slot is 3:2 and gets its height from
       its width; the video slots are vertical and keep their own aspect, so
       they need that same height as a number to size from. */
    .conveyor { --slot-w: calc((100vw - var(--cols) * var(--slot-gap)) / var(--cols)); }

    .slot {
      flex: none;
      width: var(--slot-w);
      /* 3:2, because that is what the photographs are. Cropping 3:2 sources
         into a 4:3 box threw away 11% of every frame for no reason. */
      aspect-ratio: 3 / 2;
      margin-right: var(--slot-gap);
      display: flex;
      align-items: center;
      justify-content: center;
      /* Lit top edge, per DESIGN.md. No drop shadow. */
      background: var(--bg-raised);
      border-top: 1px solid var(--border-lit);
      border-left: 1px solid var(--border);
      border-right: 1px solid var(--border);
      border-bottom: 1px solid var(--ink);
      border-radius: var(--r-md);
      overflow: hidden;
    }

    .slot span {
      color: var(--steel-700);
      font-size: 14px;
    }

    /* cover, not contain: the slot is a window onto the photo, and the crop
       was already chosen per image when the file was generated. */
    .slot img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }

    /* ---------- the vertical slots ----------
       Three of the belt's items are 9:16 video, and they are NOT cropped to
       3:2. The brief was to keep them whole and match the height, so each is
       as wide as its own aspect makes it at the photos' height - about 138px
       against a 245px-tall photo slot at 1440px. They read as a narrow card
       between wide ones, which is the point.

       The frame comes from .slot, so the belt still looks like one belt. A
       clip that is not 9:16 sets --ar inline (width over height), as
       .slot--phone does. */
    .slot--v {
      /* Both dimensions from the same number the photo slot uses, rather than
         a height plus aspect-ratio: with border-box those two disagree by the
         2px of border and the video card sits short in the row. */
      height: calc(var(--slot-w) / 1.5);
      width: calc(var(--slot-w) / 1.5 * var(--ar, .5625));
    }

    /* ---------- the phone-only slots ----------
       The hero belt is display:none under 760px and reel.js never fetches its
       clips there, so a phone sees none of that creative at all. Five of the
       belt's items ride the conveyor instead, which already runs on phones and
       already carries three clips the belt does not. No new mover: DESIGN.md says
       there is no sixth, and this adds none.

       Hidden above 760px rather than the other way round, because that is the
       belt's own breakpoint (site.css:1125, reel.js:90) and the two should
       hand over at the same width. Above it the belt does the job and showing
       these as well would be the same creative twice.

       display:none is also what keeps them free on desktop: the images are
       loading=lazy so a hidden one is never in the viewport to fetch, and the
       videos are loaded by slot-video.js off an IntersectionObserver, which a
       display:none element never fires. Neither costs a desktop visitor
       anything.

       Same two-number sizing as .slot--v above, with the ratio parameterised
       because these are not all 9:16: --ar is width over height, set inline
       per item from the same figure the belt's data-ar carries. */
    .slot--phone { display: none; }

    @media (max-width: 760px) {
      .slot--phone {
        display: flex;
        height: calc(var(--slot-w) / 1.5);
        width: calc(var(--slot-w) / 1.5 * var(--ar, 1));
      }

      /* Five more slots make the phone's set wider than the desktop's, and
         --conveyor-rate is a whole-loop time rather than a speed, so the
         no-JS fallback has to be restated here or a phone with scripts off
         would run the band at about 133px/s instead of 70. Set width at
         390px is 3210px, and 3210 / 70 = 45.9s. With JS on,
         data-marquee-speed governs at either width and this is unused.
         On .conveyor, not bare in the block: a custom property still needs a
         selector, and .conveyor__track inherits it from here. */
      .conveyor { --conveyor-rate: 45.9s; }
    }

    /* contain, not cover: the whole frame is the reason these are here, and
       the slot is already the video's shape, so nothing letterboxes. */
    .slot__v {
      width: 100%;
      height: 100%;
      object-fit: contain;
      display: block;
      background: var(--bg);
    }

    @media (max-width: 900px) { .conveyor { --cols: 2; } }
    @media (max-width: 560px) { .conveyor { --cols: 1; } }

    .logo-band {
      margin-top: calc(var(--space-md) + (var(--space-lg) - var(--space-md)) * var(--dial));
      border-top: 1px solid var(--border);
      border-bottom: 1px solid var(--border);
    }

    /* A stacked lockup matched by HEIGHT reads far smaller than a wide one,
       because almost all of its area is vertical. Matched optically instead. */
    .logo-band__set li img.is-stacked { height: calc(54px + 30px * var(--dial)); }

    .logo-band__set li img {
      display: block;
      height: calc(34px + 18px * var(--dial));              /* marks are matched by height, never width */
      width: auto;
      /* Greyscale is not a preference. A green mascot and an orange wordmark
         next to cold steel would each read as a second accent, and the palette
         allows one. Stripped of hue they become texture. */
      filter: grayscale(1) brightness(1.12) contrast(.95);
      opacity: calc(.7 + .12 * var(--dial));   /* context, not the subject */
      transition: opacity var(--dur-base) var(--ease-metal);
    }
    .logo-band__set li:hover img { opacity: 1; }

    /* Marks with a case study are links. The link is only a wrapper, so it
       lays out as the bare image did; focus brightens it as hover does. */
    .logo-band__set a { display: block; }
    .logo-band__set a:focus-visible img { opacity: 1; }

    .logo-band__set li {
      flex: none;
      padding: calc(22px + 8px * var(--dial)) clamp(calc(28px + 4px * var(--dial)), calc(4vw + 0.5vw * var(--dial)), calc(56px + 16px * var(--dial)));
      color: var(--steel-700);
      font-size: 15px;
      font-weight: 600;
      letter-spacing: -0.01em;
      white-space: nowrap;
    }

    /* ---------- capabilities ----------
       Separation by hairline, not by a repeated rounded box. The 1px grid
       gap lets the container colour through as the rules, so there is one
       border to change rather than twenty-four. */

    .capabilities,
    .verticals {
      padding: var(--space-xl) 0;
      border-top: 1px solid var(--border);
    }


    /* Same ladder as .grid. Six items divide cleanly by 3, 2 and 1, so no
       breakpoint can leave a hole. */


    /* Hover is a RIM LIGHT, not a fill: a 1px inset highlight on the top
       edge with zero blur, which DESIGN.md names as the other correct form
       of rim light alongside a 1px border. It moved off ::before because
       the arrival sweep now owns that pseudo-element, and the two need to
       be independent - hovering mid-sweep must not fight it.

       Nothing bleeds and no metal is spent. The page already carries its
       three metallic moments and six more would cancel them. */

    /* The index. .tx-etched presses it INTO the surface - a 1px hard offset
       each way, zero blur. This is the home that texture never had: it was
       built for labels and numbers, and the brief rules out the eyebrow
       labels that were the obvious alternative. */

    /* Subheads step down the steel ramp. --white is rationed to headlines;
       six of them at full white would flatten the hierarchy. */


    /* ---------- capabilities: three groups and a floor ----------
       Rendered by capabilities.js from the one data object, on the home
       page and on capabilities.html alike. Grid areas mirror the nav:

         .        brands   brands
         agencies b2b      b2c
         shared   shared   shared

       Bold, per the earlier two-panel version: group names are display
       type, the names under them a step below a section heading, one
       hairline between columns. No boxes, no fills, no radius. */

    .capabilities {
      padding: var(--space-xl) 0;
      border-top: 1px solid var(--border);
    }

    .cap-panels {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      grid-template-areas:
        ".  brands brands"
        "g1 g2     g3"
        "sh sh     sh";
      gap: 0 var(--space-md);
      margin-top: var(--space-md);
    }
    .cap-parent { grid-area: brands; }
    .cap-panel:nth-of-type(1) { grid-area: g1; }
    .cap-panel:nth-of-type(2) { grid-area: g2; }
    .cap-panel:nth-of-type(3) { grid-area: g3; }
    .cap-shared { grid-area: sh; }

    /* The parent label: sentence case, carried by the rule beneath it
       spanning both brand columns, not by tracking. */
    .cap-parent {
      margin: 0 0 var(--s-6);
      padding-bottom: var(--s-3);
      border-bottom: 1px solid var(--border);
      font-size: 15px;
      font-weight: 500;
      color: var(--steel-700);
    }

    .cap-panel { min-width: 0; }
    .cap-panel + .cap-panel {
      padding-left: var(--space-md);
      margin-left: calc(-1 * var(--space-md));
      border-left: 1px solid var(--border);
    }

    /* Display scale, sized for a third of the width rather than half. */
    .cap-panel__label {
      margin: 0;
      font-size: clamp(40px, 4.6vw, 68px);
      font-weight: 800;
      line-height: .95;
      letter-spacing: -.03em;
      color: var(--white);
    }

    .cap-panel__tag {
      margin: var(--s-4) 0 0;
      font-size: clamp(16px, 1.3vw, 19px);
      line-height: 1.5;
      min-height: calc(2 * 1.5em);    /* two lines reserved: a one-line descriptor must not pull its list up */
      color: var(--steel-300);
      max-width: 30ch;
    }

    .cap-panel__list,
    .cap-shared__list {
      list-style: none;
      margin: var(--space-sm) 0 0;
      padding: 0;
    }

    .cap-panel__list li { border-top: 1px solid var(--border); }
    .cap-panel__list li:last-child { border-bottom: 1px solid var(--border); }

    /* Every row is one link: number, then name. The row lights, the
       number brightens with it, one duration. */
    .cap-panel__list a,
    .cap-shared__list a {
      display: flex;
      align-items: baseline;
      gap: var(--s-4);
      padding: var(--s-4) 0;
      text-decoration: none;
      color: var(--steel-050);
      transition: color var(--dur-quick) var(--ease-hover);
    }
    .cap-panel__list a:hover,
    .cap-panel__list a:focus-visible,
    .cap-shared__list a:hover,
    .cap-shared__list a:focus-visible { color: var(--white); }

    .cap-panel__list .cap-name,
    .cap-shared__list .cap-name {
      font-size: clamp(18px, 1.5vw, 22px);
      font-weight: 600;
      line-height: 1.2;
      letter-spacing: -.01em;
    }

    .cap-panel__list .cap-n,
    .cap-shared__list .cap-n {
      flex: none;
      min-width: 2ch;
      font-size: 13px;
      font-weight: 700;
      color: var(--steel-700);
      font-variant-numeric: tabular-nums;
      transition: color var(--dur-quick) var(--ease-hover);
    }
    .cap-panel__list a:hover .cap-n,
    .cap-panel__list a:focus-visible .cap-n,
    .cap-shared__list a:hover .cap-n,
    .cap-shared__list a:focus-visible .cap-n { color: var(--steel-100); }

    /* The floor. A rule above, the four laid across. */
    .cap-shared {
      margin-top: var(--space-md);
      padding-top: var(--space-md);
      border-top: 1px solid var(--border);
    }
    .cap-shared__label {
      margin: 0;
      font-size: clamp(24px, 2.2vw, 32px);
      font-weight: 700;
      letter-spacing: -.02em;
      color: var(--white);
    }
    .cap-shared__tag {
      margin: var(--s-2) 0 0;
      font-size: clamp(15px, 1.2vw, 17px);
      color: var(--steel-300);
    }
    .cap-shared__list {
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 0 var(--s-6);
    }
    .cap-shared__list li { border-top: 1px solid var(--border); }

    @media (max-width: 900px) {
      .cap-panels {
        grid-template-columns: 1fr;
        grid-template-areas: "g1" "brands" "g2" "g3" "sh";
      }
      .cap-parent { margin-top: var(--space-lg); }
      .cap-panel + .cap-panel { padding-left: 0; margin-left: 0; border-left: 0; }
      .cap-panel + .cap-panel,
      .cap-panel:first-of-type + .cap-parent { margin-top: var(--space-lg); }
      .cap-shared__list { grid-template-columns: 1fr; gap: 0; }
      .cap-panel__tag { min-height: 0; }
    }

    /* No JS: the section never renders. Say so rather than leave a hole. */
    .cap-panels:empty + .cap-fallback { display: block; }
    .cap-fallback { display: none; margin-top: var(--space-md); }

    /* ---------- the capability page ----------
       capability.html, rendered by capabilities.js from ?c={slug}.
       Deliberately plain: the content is being written later. */
    /* padding-block, not the shorthand: this element is also .wrap, and the
       shorthand zeroes its gutter. */
    .cap-page { padding-block: var(--space-lg) var(--space-xl); }
    /* capabilities.js puts this class on two different elements: the <p>
       breadcrumb above the title (number, then group name, on one line), and
       a <div> in the unknown-slug branch that wraps a heading, a tagline and
       a list. The flex row below was written for the first. Applied to the
       second it laid the heading, tagline and list out side by side, and the
       row could not shrink: 508px wide inside a 320px viewport, which is the
       188px of sideways scroll. Scoped to the <p> so the wrapper is a normal
       block. */
    p.cap-page__group {
      display: flex;
      align-items: baseline;
      gap: var(--s-3);
      margin: 0 0 var(--s-4);
      font-size: 15px;
      color: var(--steel-700);
    }
    p.cap-page__group .cap-n { font-weight: 700; font-variant-numeric: tabular-nums; }
    .cap-page__title {
      margin: 0;
      font-size: clamp(44px, 6vw, 96px);
      line-height: .98;
      letter-spacing: -.03em;
      max-width: 14ch;
    }
    .cap-page__tag {
      margin: var(--s-5) 0 0;
      font-size: clamp(17px, 1.5vw, 21px);
      line-height: 1.5;
      color: var(--steel-300);
      max-width: 48ch;
      /* The unknown-slug branch quotes ?c= back at the visitor, and a slug is
         one token with nothing to break on. A 500-character one measured 5671px
         of scrollWidth in a 280px column. */
      overflow-wrap: anywhere;
    }
    .cap-page__body {
      margin-top: var(--space-md);
      max-width: 64ch;
      font-size: clamp(17px, 1.4vw, 19px);
      line-height: 1.65;
      color: var(--text);
    }
    .cap-page__body p + p { margin-top: var(--s-4); }
    .cap-page__body--empty { color: var(--steel-500); }
    .cap-page__cta { margin-top: var(--space-md); }
    .cap-page__more {
      margin-top: var(--space-lg);
      padding-top: var(--space-md);
      border-top: 1px solid var(--border);
    }
    .cap-page__more-head,
    .cap-page__group-name {
      margin: 0;
      font-size: clamp(22px, 2vw, 30px);
      font-weight: 700;
      letter-spacing: -.02em;
      color: var(--white);
    }
    .cap-page__group-tag { margin: var(--s-2) 0 0; color: var(--steel-300); }
    .cap-page__list {
      list-style: none;
      margin: var(--s-6) 0 0;
      padding: 0;
      display: grid;
      /* min() so the track can fall below 260px rather than forcing the grid
         wider than its container on a narrow phone. */
      grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
      gap: 0 var(--s-6);
    }
    .cap-page__list li { border-top: 1px solid var(--border); }
    .cap-page__list a {
      display: flex;
      align-items: baseline;
      gap: var(--s-3);
      padding: var(--s-3) 0;
      color: var(--steel-050);
      text-decoration: none;
      transition: color var(--dur-quick) var(--ease-hover);
    }
    .cap-page__list a:hover,
    .cap-page__list a:focus-visible { color: var(--white); }
    .cap-page__list .cap-n { font-size: 12px; font-weight: 700; color: var(--steel-700); font-variant-numeric: tabular-nums; }
    .cap-page__all { margin-top: var(--space-md); display: grid; gap: var(--space-md); }

    /* ---------- who we work with: the vertical cards ----------
       Each card is one <a>. The whole tile is the target, so there is no
       second click target inside it and nothing to miss on a phone.

       Built on .panel's rule, not a fill: lit top edge, --border down the
       sides, --ink along the bottom so it falls away into the page. Square
       corners, because the only border-radius on this site is the 50% on
       two circles and DESIGN.md rules out a single radius applied to
       everything. */

    /* ---- the rail ----------------------------------------------------
       Two states, one element.

       Declared here as a scroll-snap strip: with JS off, or under
       prefers-reduced-motion, this is the whole component - six readable
       cards at full strength you can scroll with a trackpad, a finger or
       shift-wheel, because the browser already knows how.

       vertical-rail.js adds .vrail--loop and the strip becomes a track:
       a slow, continuous right-to-left loop that the script drives with
       one number (--x) and fades with one number per card (--k). Those
       rules are further down under "the loop". */
    .vrail {
      display: flex;
      gap: var(--space-sm);
      margin: var(--space-md) 0 0;
      padding: 0;
      list-style: none;
      overflow-x: auto;
      overscroll-behavior-x: contain;
      /* proximity, NOT mandatory. Mandatory has to land on a snap point at
         all times, so on load it immediately ate the centring padding below
         and pulled a single card to the middle - the exact thing the
         padding exists to prevent. Proximity leaves the two-card straddle a
         stable resting position and still settles a near-miss. */
      scroll-snap-type: x proximity;
      /* No leading pad: the cards start on the same line as the heading.
         The trailing pad only matters in the scroll-strip state, where it
         lets the last card scroll clear of the right edge. The loop zeroes
         it. */
      padding-inline-start: 0;
      padding-inline-end: max(0px, calc(100% - 2 * var(--vcard-w) - var(--space-sm)));
      scrollbar-width: none;
      /* Room for the lifted centre cards to rise into without clipping. */
      padding-block: 18px;
    }
    .vrail::-webkit-scrollbar { display: none; }

    .vrail > li {
      flex: 0 0 var(--vcard-w);
      scroll-snap-align: center;
      display: flex;
    }

    /* Sized down from the grid it replaced: two of these sit in the middle
       at 1440 with the neighbour showing at the edge, which is what makes it
       read as a rail rather than a row that happens to scroll.

       Declared on the component, not :root. :root is the site's token
       layer; a card width is this component's business, the same way
       .conveyor keeps --cols and --slot-gap to itself. */
    .verticals { --vcard-w: clamp(calc(258px + 42px * var(--dial)), calc(27vw + 4vw * var(--dial)), calc(384px + 116px * var(--dial))); }

    /* The resting card. No transform, no transition, no falloff: this is
       what a no-JS or reduced-motion visitor gets, and it has to be
       complete on its own. */
    .vcard {
      --k: 0;
      display: flex;
      flex-direction: column;
      width: 100%;
      background: var(--bg-raised);
      border-top: 1px solid var(--border-lit);
      border-left: 1px solid var(--border);
      border-right: 1px solid var(--border);
      border-bottom: 1px solid var(--ink);
      border-radius: var(--r-md);
      overflow: hidden;
    }

    /* ---- the loop ----------------------------------------------------
       vertical-rail.js puts .vrail--loop on the rail once it has cloned
       the cards and taken over. The strip stops scrolling and becomes a
       window onto a track that moves under it. */
    .vrail--loop {
      overflow: hidden;
      scroll-snap-type: none;
      padding-inline: 0;
      /* Draggable, on the marquee bands' rules: pan-y keeps vertical page
         scrolling working on touch while horizontal drags come to us. */
      cursor: grab;
      touch-action: pan-y;
      /* Cards dissolve at the edges rather than getting cut off. The
         fade is inside the rail's own box, so nothing bleeds into the
         gutter - a mask, not a glow. */
      -webkit-mask-image: linear-gradient(to right, transparent, #000 14%, #000 86%, transparent);
              mask-image: linear-gradient(to right, transparent, #000 14%, #000 86%, transparent);
    }

    .vrail--loop.is-dragging { cursor: grabbing; }
    /* Without this a drag selects the card copy instead of moving the track. */
    .vrail--loop.is-dragging,
    .vrail--loop.is-dragging * { user-select: none; }
    /* The case-study link is a link, not a drag handle. */
    .vrail--loop .vcard__case { cursor: pointer; }

    /* --x is the track position, one value on the rail that every card
       shares. --k is per card: 0 at the centre of the rail, 1 at either
       edge. The brief spends it two ways - opacity to 30% and scale to
       90% at the edges - and the lit top edge gets a third: it is the
       one thing that gets BRIGHTER at the centre rather than just less
       dim. Rim light, per DESIGN.md.

       No transition. The script retargets --k every frame, so a
       transition here would only lag the card behind its own position.
       transform and opacity only, so the loop runs on the compositor. */
    .vrail--loop .vcard {
      transform: translateX(var(--x, 0px)) scale(calc(1 - 0.10 * var(--k, 0)));
      opacity: calc(1 - 0.70 * var(--k, 0));
      border-top-color: color-mix(in srgb, var(--border-lit), var(--steel-100) calc((1 - var(--k, 0)) * 100%));
      will-change: transform, opacity;
    }

    /* Seconds for one card pitch to pass a fixed point - how long a card
       takes to cross the centre. Drift, not a slide: the brief is 8-10s.
       Read by vertical-rail.js; tune it here, not there. */
    .verticals { --vrail-dwell: 9s; }

    /* One card at a time on a phone. The card fills most of the rail so
       the one in the middle is the one you read, with its neighbours
       just showing at the edges. Same falloff, same pace. */
    @media (max-width: 640px) {
      .verticals { --vcard-w: 82%; }
    }

    /* No hover state, and no cursor change: these cards do not go anywhere
       yet. A tile that lights up under the pointer promises a click it
       cannot honour, which is the same rule the footer's social icons are
       waiting on. When the vertical pages exist, wrap each card's contents
       in an <a> and the hover treatment comes back with it. */

    /* Reduced motion never gets .vrail--loop - the script bails before
       adding it - so the base rules above are the whole story: the strip,
       every card full size and full strength, scrolling and nothing else.
       Belt and braces for the one case where the preference flips while
       the class is on. */
    @media (prefers-reduced-motion: reduce) {
      .vrail--loop .vcard {
        transform: translateX(var(--x, 0px));
        opacity: 1;
      }
    }

    /* ---- arrows ----
       Steel, not amber. Secondary actions do not get warmth, and the page
       spends its one amber element on the CTA. */
    .vrail-nav {
      display: flex;
      gap: var(--s-2);
      margin-top: var(--space-sm);
    }

    .vrail-btn {
      width: calc(44px + 8px * var(--dial));
      height: calc(44px + 8px * var(--dial));
      display: grid;
      place-items: center;
      background: transparent;
      color: var(--steel-100);
      border: 1px solid var(--border-lit);
      border-radius: 50%;
      cursor: pointer;
      transition: color var(--dur-base) var(--ease-metal),
                  border-color var(--dur-base) var(--ease-metal);
    }
    .vrail-btn svg { width: 20px; height: 20px; stroke-width: 1.5; }
    .vrail-btn:hover:not(:disabled) { color: var(--white); border-color: var(--steel-700); }

    /* ---- media: photo and icon share every dimension ----
       The two treatments have to read as one system, so the box is defined
       here once and the children differ, never the frame. */
    .vcard__media {
      display: block;
      aspect-ratio: 16 / 9;
      border-bottom: 1px solid var(--border);
      background: var(--charcoal);
      overflow: hidden;
    }

    .vcard__media img {
      display: block;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    /* The no-photo treatment. Same box, same hairline, one centred outline
       mark. Deliberately not a placeholder: nothing here is waiting on an
       asset. */
    .vcard__media--mark {
      display: grid;
      place-items: center;
    }

    .vcard__mark {
      width: calc(68px + 16px * var(--dial));
      height: calc(68px + 16px * var(--dial));
      /* Thinner as it scales up: 2 is Tabler's weight at 24px, and holding
         it at 84px turns a line drawing into a slab. */
      stroke-width: 1;
      color: var(--steel-500);
    }

    .vcard__body {
      display: block;
      /* The bottom padding lives here, not on the footer's margin, because
         the footer's top margin is auto and auto collapses to 0 the moment
         the copy fills the card - which would butt the tags into the rule. */
      padding: calc(22px + 6px * var(--dial)) calc(20px + 6px * var(--dial)) calc(20px + 4px * var(--dial));
    }

    /* An <h3>, not a <span>, since the merge. Six industry names is six
       commercial-intent phrases, and as spans they carried no structural
       weight under the section's h2 - the same finding as the capability
       titles. margin is the only thing a heading adds that a span did not,
       so it is the only thing reset. Rendered output is unchanged. */
    /* The name is what the card is scanned for, so it takes the house
       heading treatment rather than overriding it down to something
       lighter than the body copy underneath it. 800 and --text-strong are
       what every other h1/h2/h3 on the site carries; this was the only
       heading opting out. */
    .vcard__name {
      margin: 0 0 calc(12px + 2px * var(--dial));
      display: block;
      font-size: clamp(calc(18px + 4px * var(--dial)), calc(1.7vw + 0.3vw * var(--dial)), calc(22px + 8px * var(--dial)));
      font-weight: 800;
      letter-spacing: -0.035em;
      line-height: 1.1;
      color: var(--text-strong);
    }

    /* Problem and fix are both body copy, so both sit on the text ramp.
       The fix is the one that has to land, so it takes the brighter step. */
    .vcard__problem {
      display: block;
      font-size: calc(15px + 1px * var(--dial));
      line-height: 1.5;
      color: var(--text);
      margin-bottom: calc(10px + 2px * var(--dial));
    }

    .vcard__fix {
      display: block;
      font-size: calc(15px + 1px * var(--dial));
      line-height: 1.55;
      color: var(--steel-100);
    }

    /* ---- capability tags ----
       Steel, not amber: warmth is spent once per viewport and the page
       already spends it on the CTA. Sentence case, because the brief rules
       out tracked-out all-caps labels. */
    .vcard__tags {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin-top: var(--s-4);
    }

    .vtag {
      font-size: calc(12px + 1px * var(--dial));
      line-height: 1;
      color: var(--steel-500);
      border: 1px solid var(--border);
      border-radius: var(--r-pill);
      padding: calc(6px + 1px * var(--dial)) calc(9px + 3px * var(--dial));
    }

    /* ---- footer row ----
       margin-top:auto pins it to the bottom edge, so the hairline lands on
       the same line across a row however long the copy above it runs. */
    /* margin-top:auto pins this to the bottom of the card. The grid already
       stretches every card in a row to the same height, but without this the
       footer floats up behind whichever card has the shortest copy, and the
       hairline lands at three different heights across the row. */
    /* Sits beside .vcard__scope in the foot's existing flex row, which
       already had the gap for a second child. A lit underline rather than a
       colour change: the rim-light move, so the amber stays on the CTA. */
    .vcard__case {
      flex: none;
      margin-left: auto;
      font-size: calc(12px + 1px * var(--dial));
      color: var(--steel-100);
      text-decoration: none;
      border-bottom: 1px solid var(--border-lit);
      padding-bottom: 1px;
      transition: color var(--dur-base) var(--ease-metal),
                  border-color var(--dur-base) var(--ease-metal);
    }
    .vcard__case:hover { color: var(--white); border-color: var(--steel-500); }
    .vcard__case:focus-visible { outline: 2px solid var(--steel-100); outline-offset: 3px; }

    .vcard__foot {
      display: flex;
      align-items: baseline;
      gap: 16px;
      margin: auto calc(20px + 6px * var(--dial)) 0;
      padding: calc(13px + 1px * var(--dial)) 0 calc(20px + 4px * var(--dial));
      border-top: 1px solid var(--border);
    }

    .vcard__scope {
      font-size: calc(12px + 1px * var(--dial));
      /* --steel-500, not --text-muted (= --steel-700). The cards sit on
         --bg-raised, where steel-700 measures 4.05 and fails WCAG AA. Same
         step as .tq-what and .cform__note: a token that clears AA on the
         page ground by 0.01 does not clear it on the raised one. */
      color: var(--steel-500);
    }

    @media (max-width: 380px) {
      .vcard__body,
      .vcard__foot { padding-left: 0; padding-right: 0; }
      .vcard__body { padding: var(--space-sm) 18px 0; }
      .vcard__foot { margin-left: 18px; margin-right: 18px; }
    }

    /* The line under the grid. Not a card and deliberately not styled as
       one: it widens the frame rather than adding a seventh vertical. */
    .vgrid-note {
      margin: var(--space-md) 0 0;
      padding-top: var(--space-sm);
      border-top: 1px solid var(--border);
      font-size: clamp(calc(15px + 2px * var(--dial)), calc(1.2vw + 0.2vw * var(--dial)), calc(17px + 4px * var(--dial)));
      line-height: 1.55;
      color: var(--steel-100);
      max-width: 72ch;
    }
    .vgrid-note a { color: var(--white); }

    /* ---------- who we work with ----------
       This began as a grid sharing .cap-grid's rules with the capabilities
       section. That section is no longer on this page - the rail replaced
       it - and .cap-grid went with it, so these rules stand on their own
       now and the sweep that keyed off it is gone. Nothing here inherits;
       what the rail needs is declared here. */

    .verticals .lead { margin-top: 20px; }

    /* The problem line is BODY COPY, not meta, so it sits at --text on the
       steel ramp. It reads as a caption in this slot and the temptation is
       to hand it .meta, but DESIGN.md puts --steel-700 and darker off
       limits for anything you actually have to read - it is for labels. */
    .vert-line {
      color: var(--text);
      font-size: 15px;
      line-height: 1.55;
      margin-bottom: 12px;
    }

    /* The problem lines run to two or three lines depending on the vertical,
       which left the scope rules sitting at three different heights across a
       row - a lattice with a ragged inner edge. Making the cell a column and
       pushing the scope down with margin-top:auto lands every rule on the
       same line, so the grid reads as one ruled surface rather than six
       independently sized boxes. */

    /* The sub-verticals underneath ARE meta - scanned, not read - so this
       one is correctly on --text-muted. Commas rather than middle dots:
       the brief rules dotted meta strings out by name. */
    .vert-scope {
      margin-top: auto;
      color: var(--text-muted);
      line-height: 1.5;
      border-top: 1px solid var(--border);
      padding-top: 12px;
    }

    /* At 1-up the cards are full width, so the problem line would run to
       the ~90ch the viewport allows. Body copy is capped at 62ch site-wide
       for the same reason. */
    @media (max-width: 600px) {
      .vert-line { max-width: 62ch; }
    }


    /* ---------- the capabilities arrival sweep ----------
       Six gold hairlines, one per cell, lit in reading order and let go as
       the pass moves on. One orchestrated moment, on arrival, once.

       This replaced a beam that walked the grid's own hairlines on a route
       of hardcoded percentages - 33.3% for the verticals, 50% for the
       middle line. That only ever described a 3-column, 2-row grid, so it
       drew lines through the middle of the cells at every other width, and
       it was measured against a wrapper that also contained the "more"
       link, which pushed it off the row line even at 3-up.

       Nothing here is positioned. The rim is the cell's own top edge, so
       there is no geometry to get wrong: 3-up, 2-up or 1-up, it lands on
       the same edges, and adding a seventh capability needs no new numbers.

       Only opacity animates, on a pseudo-element that is 1px tall. */


    /* The warmth left behind. Very low: six lit cells at any real strength
       would be six amber elements, and the palette allows one. */

    /* The sweep never runs under reduced motion - initGridSweep bails before
       it constructs anything - so both values stay at 0 on their own. This
       is belt and braces for the case where something else sets them. */
    @media (prefers-reduced-motion: reduce) {
    }

    /* ==========================================================
       INTRO — the bullet
       Base state below is the FINISHED state. The animation only
       runs while <body class="tx-grain"> carries .is-intro, which JS adds. No JS,
       or reduced motion, and the page is simply already correct.
       ========================================================== */

    .intro-layer {
      position: fixed;
      inset: 0;
      z-index: 60;
      pointer-events: none;
      display: none;
      overflow: hidden;
    }

    body.is-intro .intro-layer { display: block; }

    /* -- the muzzle blip: two hard bars crossing. no blur. -- */
    .blip {
      position: absolute;
      top: var(--nav-h);
      left: 0;
      width: 1px;
      height: 1px;
      opacity: 0;
    }

    .blip::before,
    .blip::after {
      content: "";
      position: absolute;
      left: 50%;
      top: 50%;
      background: var(--white);
      transform: translate(-50%, -50%);
    }

    .blip::before { width: 60px; height: 1px; }
    .blip::after  { width: 1px;  height: 18px; }

    /* -- the bullet: a tapered steel shard -- */
    .bullet {
      position: absolute;
      top: calc(var(--nav-h) - 10px);
      left: 0;
      width: 150px;
      height: 20px;
      transform: translateX(-260px);
      will-change: transform;
    }

    .bullet__body,
    .bullet__glint {
      position: absolute;
      inset: 0;
      clip-path: polygon(100% 50%, 76% 0, 0 0, 0 100%, 76% 100%);
    }

    /* Cylindrical metal: dark top, hot band at the centre line, dark base. */
    /* Chrome, not plastic.

       Every stop is PAIRED at an identical percentage, which produces a hard
       edge with no blend between bands. Smooth gradients read as plastic;
       abrupt value jumps read as metal, because real chrome reflects a
       cut-up environment rather than a soft falloff.

       Neutral --chrome-* ramp only (see "Two metal ramps" in DESIGN.md).
       Tonal split is roughly 36% shadow / 46% midtone / 19% highlight, with
       only 5% at the brightest band. Most of chrome is dark. */
    .bullet__body {
      background: linear-gradient(
        to bottom,
        var(--chrome-900) calc(  0% + var(--lo)), var(--chrome-900) calc(  5% + var(--lo)),
        var(--chrome-700) calc(  5% + var(--lo)), var(--chrome-700) calc( 22% + var(--lo)),
        var(--chrome-500) calc( 22% + var(--lo)), var(--chrome-500) calc( 33% + var(--lo)),
        var(--chrome-400) calc( 33% + var(--lo)), var(--chrome-400) calc( 45% + var(--lo)),
        var(--chrome-300) calc( 45% + var(--lo)), var(--chrome-300) calc( 51% + var(--lo)),
        var(--chrome-100) calc( 51% + var(--lo)), var(--chrome-100) calc( 58% + var(--lo)),
        var(--chrome-050) calc( 58% + var(--lo)), var(--chrome-050) calc( 63% + var(--lo)),
        var(--chrome-100) calc( 63% + var(--lo)), var(--chrome-100) calc( 70% + var(--lo)),
        var(--chrome-300) calc( 70% + var(--lo)), var(--chrome-300) calc( 78% + var(--lo)),
        var(--chrome-500) calc( 78% + var(--lo)), var(--chrome-500) calc( 86% + var(--lo)),
        var(--chrome-700) calc( 86% + var(--lo)), var(--chrome-700) calc( 96% + var(--lo)),
        var(--chrome-900) calc( 96% + var(--lo)), var(--chrome-900) 100%
      );
    }

    /* The airstream behind it. */
    .bullet__wake {
      position: absolute;
      right: 88%;
      top: 50%;
      width: 260px;
      height: 2px;
      transform: translateY(-50%);
      background: linear-gradient(
        to right,
        color-mix(in srgb, var(--steel-500) 0%, transparent),
        color-mix(in srgb, var(--steel-500) 55%, transparent) 70%,
        var(--steel-100)
      );
    }

    /* Friction heat building at the nose. */
    .bullet__heat {
      position: absolute;
      right: -1px;
      top: 50%;
      width: 72px;
      height: 20px;
      transform: translateY(-50%);
      clip-path: polygon(100% 50%, 44% 0, 0 22%, 0 78%, 44% 100%);
      background: linear-gradient(
        to left,
        var(--gold)        0%,
        var(--amber)      30%,
        var(--amber-burnt) 58%,
        color-mix(in srgb, var(--amber-burnt) 0%, transparent) 100%
      );
      opacity: 0;
    }

    /* Hard specular band — a sharp edge of light, not a bloom. */
    .bullet__glint {
      background: linear-gradient(
        112deg,
        color-mix(in srgb, var(--gold) 0%, transparent) 42%,
        var(--gold)  47%,
        var(--amber) 53%,
        color-mix(in srgb, var(--amber) 0%, transparent) 58%
      );
      opacity: 0;
    }

    /* ---------- intro: the animations ---------- */

    body.is-intro .site-header__rule {
      animation: rule-draw var(--t-fly) linear var(--t-fire) both;
    }

    body.is-intro .blip {
      animation: blip .18s ease-out var(--t-fire) both;
    }

    body.is-intro .bullet {
      animation: fly var(--t-fly) linear var(--t-fire) both;
    }

    body.is-intro .bullet__heat {
      animation: heat .60s ease-in .34s both;
    }

    body.is-intro .bullet__glint {
      animation: glint .2s linear var(--t-glint) both;
    }

    body.is-intro .logo {
      animation: logo-home .70s var(--ease-enter) var(--t-shift) both;
    }

    /* Two phases, two animations, because each needs its own easing — a single
       keyframe set with per-keyframe timing functions did not hold, and the
       return measured flat instead of decelerating.

       They never overlap, and the fill modes are what keep them apart:
       ticker-home is `forwards` and deliberately NOT `backwards`, so through
       its 1.15s delay it does not apply at all and ticker-ride owns the
       transform. From --t-shift it is active and, being later in the list,
       wins — which is the same instant the logo starts home. */
    body.is-intro .ticker__travel {
      animation:
        ticker-ride var(--t-fly) linear var(--t-fire) both,
        ticker-home 1.10s var(--ease-recoil) var(--t-shift) forwards;
    }

    body.is-intro .logo__inner {
      animation: name-wipe .26s var(--ease-enter) var(--t-name) both;
    }

    body.is-intro .nav > * {
      animation: nav-in .40s ease both;
      animation-delay: var(--t-nav);
    }
    body.is-intro .nav > *:nth-child(2) { animation-delay: calc(var(--t-nav) + .06s); }
    body.is-intro .nav > *:nth-child(3) { animation-delay: calc(var(--t-nav) + .12s); }
    body.is-intro .nav > *:nth-child(4) { animation-delay: calc(var(--t-nav) + .18s); }

    /* `backwards`, not `both`. The two are visually identical here, because
       page-in's `to` state (opacity 1) is already the base state — backwards
       still holds opacity 0 through the --t-page delay, which is the only
       part of the fill that is doing any work.

       `both` costs 1.6s of LCP. A forwards-filling animation never leaves its
       active phase, so as far as Chrome is concerned <main> has an opacity
       animation running on it right up until .is-intro comes off at 2400ms,
       and it will not report a largest-contentful-paint underneath one.
       LCP landed at ~2470ms — the moment the class was removed — rather than
       when the text actually became legible at ~900ms. */
    body.is-intro main,
    body.is-intro .site-footer {
      animation: page-in .55s var(--ease-enter) var(--t-page) backwards;
    }

    @keyframes rule-draw {
      from { transform: scaleX(0); }
      to   { transform: scaleX(1); }
    }

    @keyframes blip {
      0%   { opacity: 0; transform: scale(.25); }
      22%  { opacity: 1; transform: scale(1); }
      100% { opacity: 0; transform: scale(1.6); }
    }

    @keyframes fly {
      from { transform: translateX(-260px); }
      to   { transform: translateX(calc(100vw + 260px)); }
    }

    @keyframes heat {
      from { opacity: 0; }
      to   { opacity: 1; }
    }

    @keyframes glint {
      0%   { opacity: 0; transform: translateX(-70%); }
      30%  { opacity: 1; }
      100% { opacity: 0; transform: translateX(70%); }
    }

    /* Out with the bullet: same direction, same linear pace, fading up. */
    @keyframes ticker-ride {
      0%   { opacity: 0; transform: translateX(0); }
      70%  { opacity: 1; }
      100% { opacity: 1; transform: translateX(var(--ticker-ride)); }
    }

    /* Back with the logo. Leaves at the bullet's pace and decays for a full
       second; that tail is what hands over to the marquee unnoticed. */
    @keyframes ticker-home {
      from { transform: translateX(var(--ticker-ride)); }
      to   { transform: translateX(0); }
    }

    /* Logo starts centred on screen, then travels to its resting place. */
    @keyframes logo-home {
      from { transform: translateX(var(--logo-shift)); }
      to   { transform: translateX(0); }
    }

    /* The name is wiped in left-to-right, in the bullet's direction. */
    @keyframes name-wipe {
      from { clip-path: inset(0 100% 0 0); }
      to   { clip-path: inset(0 0 0 0); }
    }

    @keyframes nav-in {
      from { opacity: 0; }
      to   { opacity: 1; }
    }

    @keyframes page-in {
      from { opacity: 0; }
      to   { opacity: 1; }
    }

    /* ---------- reduced motion: no intro at all ---------- */

    @media (prefers-reduced-motion: reduce) {
      html { scroll-behavior: auto; }
      .intro-layer { display: none !important; }
      /* The marquees run off the base stylesheet, not off .is-intro, so the
         blanket rule below does not reach them. Stop all three by attribute,
         which also covers any band added later. */
      [data-marquee-track] { animation: none !important; }
      body.is-intro *,
      body.is-intro *::before,
      body.is-intro *::after {
        animation: none !important;
        transition: none !important;
      }
    }


/* Route from the home page's six capabilities to the full tab. */
.cap-more { margin: var(--space-sm) 0 0; }


    /* ---------- case study art ----------
       .case-art is a 21:9 frame declared inline on each case page, but no
       rule ever sized the image inside it - so a 1200x800 photo sat there
       at natural size and spilled out of the frame, cropped by wherever the
       flex centring happened to land. cover fits it to the frame instead. */
    .case-art img,
    .case-art--wide img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
    }

    /* The crew shot is a group portrait, so it gets a squarer frame than the
       21:9 hero - five faces in a letterbox lose their heads. */
    .case-art--wide { aspect-ratio: 3 / 2; }


/* ---------- companies who trust us ----------
   Inbound's client band, which is a heading, one line, and then just the
   logos. No cells, no boxes, no rules — the marks sit on the page with air
   around them and nothing else competing.

   Every logo is pre-keyed to a flat white silhouette on transparency, so
   there is no filter doing the monochrome work at runtime and no logo
   arriving with its own brand colour. Opacity is the only variable. */

.clients { padding: var(--space-xl) 0; border-top: 1px solid var(--border); }
.clients__head { max-width: 62ch; margin-bottom: var(--space-lg); }
.clients__head .lead { margin-top: var(--space-sm); }

.client-wall {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(calc(38px + 2px * var(--dial)), 6vw, calc(86px + 24px * var(--dial)));
  margin: 0;
  padding: 0;
  list-style: none;
}

.client-wall img {
  display: block;
  width: auto;
  opacity: calc(.48 + .22 * var(--dial));
  transition: opacity .3s ease;
}

.client-wall li:hover img { opacity: 1; }
.client-wall a { display: block; }
.client-wall a:focus-visible img { opacity: 1; }

/* Optical sizing, not mechanical. A wide wordmark and a stacked mark set to
   the same height do not read as the same size, so each shape gets its own
   cap. This is the difference between a logo wall and a row of mismatched
   images. */
.client-wall .is-wide  { max-height: clamp(calc(22px + 12px * var(--dial)), calc(2.4vw + 1.4vw * var(--dial)), calc(30px + 26px * var(--dial))); }
.client-wall .is-mid   { max-height: clamp(calc(28px + 16px * var(--dial)), calc(3vw   + 1.8vw * var(--dial)), calc(38px + 34px * var(--dial))); }
.client-wall .is-tall  { max-height: clamp(calc(46px + 26px * var(--dial)), calc(5vw   + 3vw   * var(--dial)), calc(62px + 56px * var(--dial))); }

@media (max-width: 620px) {
  .client-wall { gap: 34px 42px; }
}

/* ==========================================================
   The stack band
   Structure mapped from inboundmedia.co's press band: centred
   head, then three centred rows of marks, one per category
   (data, measurement, media). The class names still say press
   (see the note in index.html).

   This is the only light band on the site. It was paper because
   the marks it used to carry were full-colour and could not be
   keyed to one value. 2026-09-17: the marks are now greyscaled
   (see .press-wall img), so the paper is carrying the section
   on habit rather than necessity. It is kept for now because
   the colour change is also what separates this band from the
   sections either side, so it carries no hairline border. If
   the band ever goes back to --bg, add the border-top the
   neighbouring sections use.
   ========================================================== */

.press {
  padding: var(--space-xl, 96px) 0;
  background: var(--paper, #F2F0EA);
  /* The marks fly in from off the viewport edges. Clipping here rather than
     leaning on body{overflow-x:hidden} keeps the travel contained to this
     band, so nothing can widen the document mid-flight. */
  overflow: hidden;
}

.press__head {
  max-width: 62ch;
  margin: 0 auto var(--space-lg, 72px);
  text-align: center;
}

/* Dark type on the light band. --charcoal rather than --ink, which
   DESIGN.md reserves for linework on light. */
.press h2 { color: var(--charcoal, #202222); }

/* .lead is capped at 62ch and left-aligned by default; both have to be
   undone or the line sits off-centre under a centred heading. */
.press__head .lead {
  max-width: none;
  margin-top: var(--space-sm, 28px);
  color: var(--steel-900, #50636E);
}

/* One grid, four across. This is the layout the wall shipped with in
   74ec84e and it is restored on instruction: sixteen marks, four rows,
   the reference gap and the reference cap. The interim version (three
   flex lists, one per category) is gone along with the categories.

   Held inside .wrap at the reference 1120px so the outer columns still
   read as the edges of one set rather than four separate lists. */
.press-wall {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  align-items: center;
  justify-items: center;
  gap: clamp(30px, 3.6vw, 52px) clamp(22px, 3vw, 44px);
  max-width: 1120px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

/* Sized by HEIGHT, with width only as an upper bound. One width cap
   renders a 2.5:1 lockup 68% taller than a 5:1 one and the row stops
   reading as a set; capping the height is how a logo wall is balanced,
   and it makes optical size independent of how long each wordmark is.

   height, not max-height: with height:auto the marks are 0px tall until
   the file arrives — the width/height attributes cannot reserve a box
   when the width side is left to resolve from an image that has not
   loaded — and they are loading="lazy", so the section grew the moment
   a visitor scrolled to it and every anchor below it moved. A declared
   height gives the attributes a side to work from.

   FULL COLOUR, on instruction. The greyscale that was here read every
   mark as texture; these are the platforms the work runs on and they
   are meant to be recognised, which is what their colour is for. The
   band sits on paper, where sixteen brand colours read as a logo wall
   and not as sixteen competing accents. */
.press-wall img {
  display: block;
  width: auto;
  height: clamp(32px, 3.4vw, 46px);
  max-width: clamp(150px, 18vw, 230px);
}

/* Optical sizing. A mark that is nearly square — the Salesforce cloud,
   the Google Tag Manager and Looker Studio icons — capped to the same
   HEIGHT as a long wordmark renders at a third of its width and reads as
   a dot beside Meta. A taller cap puts them at the same visual weight.

   This sets height, not max-height. The reference had max-height here,
   which a declared height on the rule above ignores outright — so the
   taller cap never actually applied and Salesforce shipped as a dot.
   Same intent, now in effect. */
.press-wall img.is-tall { height: clamp(44px, 4.6vw, 62px); }

/* Two across under 900px, one under 560px — the reference ladder. The
   reveal in motion.js reads each mark's rendered position rather than its
   column index, so at every width a mark still enters from whichever edge
   it is nearer to. */
@media (max-width: 900px) {
  .press-wall { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  .press-wall { grid-template-columns: 1fr; }
}

/* ==========================================================
   Testimonials band
   Structure mapped from inboundmedia.co's .testimonials-sec.

   Their measurements, kept: 860px quote column, 28px quote at
   1.36 line-height, 73px round author mark, 14px attribution
   tracked to 0.1em, 5px dots on 8px centres, 500ms ease-in-out
   cross-fade. Their colours, not kept — see tokens.css.

   Two states in one stylesheet:
     - default        : every quote stacked and readable. This is
                        what you get with JS off, and it is not a
                        broken version of anything.
     - .is-slider     : added by testimonial-slider.js. Slides
                        collapse onto one grid cell and cross-fade.

   The grid-stack is deliberate. Absolutely positioning the slides
   would need a measured height on the viewport and would jump when
   a long quote follows a short one; putting every slide in
   `grid-area: 1/1` makes the container size itself to the tallest
   quote once, with no JS measurement and no reflow on change.
   ========================================================== */

.testimonials {
  /* --space-lg, not the --space-xl every other section gets. This band is a
     single centred block rather than a heading plus content, so the same
     padding reads as far more air around far less structure - 109px top and
     bottom against one paragraph. --space-lg also lands on the reference's
     own 90px. */
  padding: var(--space-lg) 0;
  /* Panel doctrine from tokens.css, run full-bleed: lit top edge,
     bottom edge falls away into the page. No shadow. */
  background: var(--bg-raised);
  border-top: 1px solid var(--border-lit);
  border-bottom: 1px solid var(--ink);
}

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

/* Stacked fallback: quotes need air between them, and a hairline so
   two centred paragraphs do not read as one run-on. The slider state
   below strips both, because then only one is ever on screen. */
/* Applied twice over - margin and padding - so this is the gap between two
   stacked quotes, not half of it. At --space-lg that was 176px. */
.tq-item + .tq-item {
  margin-top: var(--space-md);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border);
}

.tq-fig {
  margin: 0;
  text-align: center;
}

.tq-quote {
  max-width: calc(860px + 300px * var(--dial));
  margin: 0 auto;
}

.tq-quote p {
  margin: 0;
  /* Lands on the reference's 28px at desktop and steps down cleanly
     rather than at a breakpoint. */
  font-size: clamp(calc(19px + 2px * var(--dial)), calc(1.9vw + 0.4vw * var(--dial)), calc(28px + 6px * var(--dial)));
  line-height: calc(1.36 - 0.04 * var(--dial));
  /* Stepped down from --white on purpose. Rule 2 in DESIGN.md: white
     is for headlines, and a quote this long set at peak white would
     out-shout the h2 it sits under. --steel-050 is the subhead step. */
  color: var(--steel-050);
  letter-spacing: -0.01em;
  text-wrap: pretty;
}

/* The attribution belongs to the quote above it, so it sits close enough to
   read as attached to it. At --space-md the 73px mark floated alone in the
   middle of the band with 64px of nothing on either side, which is what made
   the section feel empty rather than composed. */
.tq-by {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-sm);
}

/* The reference's round author photo. Holding initials until real
   images exist — same box either way, so dropping an <img> in here
   changes nothing else. */
.tq-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(73px + 15px * var(--dial));
  height: calc(73px + 15px * var(--dial));
  margin-bottom: calc(12px + 2px * var(--dial));
  border-radius: 50%;
  background: var(--charcoal);
  /* Rim light, not glow: a lit top edge tight to the form. */
  border-top: 1px solid var(--border-lit);
  border-left: 1px solid var(--border);
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--ink);
  color: var(--steel-500);
  font-size: 15px;
  letter-spacing: 0.08em;
  overflow: hidden;
}

.tq-mark img { width: 100%; height: 100%; object-fit: cover; }

.tq-who {
  color: var(--steel-300);
  font-size: calc(14px + 1px * var(--dial));
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Meta, not body copy. --steel-500 rather than the --steel-700 the ramp
   would suggest, because this band is --bg-raised, not --bg: steel-700
   clears WCAG AA against the page ground by 0.01 (4.51 against 4.50) and
   fails against the raised one at 4.05. steel-500 is 5.75 there and still
   reads a clear step below the attribution above it. */
.tq-what {
  color: var(--steel-500);
  font-size: calc(14px + 1px * var(--dial));
}

/* ---------- slider state ---------- */

.tq.is-slider .tq-list {
  display: grid;
  /* Height is set per slide by the script and eased between them, which is
     what the reference does. Without it the grid cell sizes to the TALLEST
     quote and every shorter one leaves its difference as dead space under
     the attribution - 111px of it between the longest and shortest of the
     four, which reads as a bug rather than as a slider.

     No height here on purpose: until the script sets one the cell is auto,
     i.e. the tallest slide, which is the safe fallback. auto does not
     interpolate, so the first assignment snaps instead of animating. */
  overflow: hidden;
  transition: height .5s ease-in-out;
}

.tq.is-slider .tq-item {
  grid-area: 1 / 1;
  /* Natural height, not stretched to the cell - the script measures this. */
  align-self: start;
  margin: 0;
  padding: 0;
  border-top: 0;
  opacity: 0;
  /* visibility, not display: display:none would drop the slide out of
     the grid and let the container shrink to the current quote, which
     is the height jump the grid-stack exists to avoid. The delay holds
     it visible for the length of the fade out. */
  visibility: hidden;
  transition: opacity .5s ease-in-out, visibility 0s linear .5s;
}

.tq.is-slider .tq-item.is-current {
  opacity: 1;
  visibility: visible;
  transition: opacity .5s ease-in-out;
}

/* ---------- dots ---------- */

.tq-dots {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;          /* the reference's 5px dot on 8px margins */
  margin: var(--space-sm) 0 0;
  padding: 0;
  list-style: none;
}

.tq-dot {
  display: block;
  width: 5px;
  height: 5px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--grey);
  cursor: pointer;
  transition: background-color .3s ease, transform .3s ease;
}

.tq-dot:hover { background: var(--steel-300); }

.tq-dot[aria-current="true"] {
  background: var(--steel-050);
  transform: scale(1.4);
}

/* The dot is 5px. A focus ring drawn tight to it is invisible, so it
   gets pushed out to something you can actually see. */
.tq-dot:focus-visible {
  outline: 1px solid var(--steel-100);
  outline-offset: 6px;
}

@media (max-width: 620px) {
  .tq-mark { width: 56px; height: 56px; font-size: 13px; }
  .tq-who  { letter-spacing: 0.06em; }
}

/* Reduced motion: the slider still works, it just cuts instead of
   fading. Nothing here moves on its own in either case. */
@media (prefers-reduced-motion: reduce) {
  .tq.is-slider .tq-list,
  .tq.is-slider .tq-item,
  .tq.is-slider .tq-item.is-current { transition: none; }
  .tq-dot { transition: none; }
}

/* ==========================================================
   Cookie consent: the banner and the settings panel (consent.js)

   Epsilon's layout (scouted 2026-10-27), this site's surface. A full-
   width bar fixed to the bottom, copy on the left and three buttons
   stacked on the right at desktop, everything in one column below
   900px. Separation is the lit top edge every panel here uses, not
   their 18px grey shadow: DESIGN.md rules shadows out.

   The three buttons are deliberately the same weight, Reject and
   Accept identical. None is amber: the hero's "See if you qualify" is
   the viewport's one amber element and the banner sits in the same
   view. The panel is a native <dialog>, so focus is held inside it and
   Esc closes it with no code here.

   Layer 55: over the nav dropdown (50), under the intro (60), so the
   bullet plays over the bar rather than the bar over the bullet.
   ========================================================== */

.consent {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 55;
  background: var(--bg-raised, #161A1B);
  border-top: 1px solid var(--border-lit, #4A4C4D);
  color: var(--text, #9AAFB8);
}
.consent[hidden] { display: none; }

.consent__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(240px, 300px);
  align-items: center;
  gap: var(--s-4, 16px) var(--s-12, 48px);
  padding-top: var(--s-6, 24px);
  padding-bottom: var(--s-6, 24px);
}

.consent__text {
  margin: 0;
  max-width: 68ch;
  font-size: var(--fs-0, 16px);
  line-height: 1.55;
}
.consent__text strong { color: var(--steel-050, #D4E5EA); font-weight: 600; }
.consent__text a,
.consent-panel a {
  color: var(--steel-050, #D4E5EA);
  text-decoration: underline;
  text-decoration-color: var(--steel-900, #50636E);
  text-underline-offset: 3px;
  transition: text-decoration-color var(--dur-quick, 0.18s) var(--ease-hover, ease);
}
.consent__text a:hover,
.consent-panel a:hover { text-decoration-color: var(--steel-050, #D4E5EA); }

.consent__actions {
  display: grid;
  gap: var(--s-2, 8px);
}

/* One button, two fills. Pills, like every other button on the site. */
.consent__btn {
  font: inherit;
  font-size: calc(14px + 1px * var(--dial, 0.32));
  font-weight: 600;
  line-height: 1.2;
  padding: var(--s-3, 12px) var(--s-5, 20px);
  border-radius: var(--r-pill, 10px);
  border: 1px solid var(--border-lit, #4A4C4D);
  background: transparent;
  color: var(--steel-100, #B4CAD2);
  cursor: pointer;
  transition:
    background-color var(--dur-quick, 0.18s) var(--ease-hover, ease),
    border-color var(--dur-quick, 0.18s) var(--ease-hover, ease),
    color var(--dur-quick, 0.18s) var(--ease-hover, ease);
}
.consent__btn--solid {
  background: var(--charcoal, #202222);
  color: var(--steel-050, #D4E5EA);
}
.consent__btn:hover {
  border-color: var(--steel-700, #6B7C86);
  color: var(--white, #E8F1F2);
}
.consent__btn--solid:hover {
  background: color-mix(in srgb, var(--steel-300, #9AAFB8) var(--alpha-hair, 10%), var(--charcoal, #202222));
}

/* Below 900px the copy takes the full width and the buttons go into a
   row under it, so on a phone the bar is about a quarter of the screen
   rather than Epsilon's 39%. */
@media (max-width: 900px) {
  .consent__inner {
    grid-template-columns: minmax(0, 1fr);
    padding-top: var(--s-4, 16px);
    padding-bottom: var(--s-4, 16px);
  }
  .consent__actions { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .consent__text { font-size: 15px; line-height: 1.5; }
  .consent__btn { padding: var(--s-3, 12px) var(--s-2, 8px); }
}

/* The footer's re-entry point, written in by consent.js. A button,
   because it opens something rather than going somewhere, dressed
   exactly as the links beside it. */
.foot-legal__btn {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  transition: color var(--dur-quick, 0.18s) var(--ease-hover, ease);
}
.foot-legal__btn:hover,
.foot-legal__btn:focus-visible { color: var(--steel-050, #D4E5EA); }

/* ---------- the settings panel ---------- */

.consent-panel {
  width: min(575px, calc(100% - 2 * var(--gutter, 20px)));
  max-height: calc(100dvh - 2 * var(--s-10, 40px));
  padding: 0;
  overflow: hidden;
  color: var(--text, #9AAFB8);
  background: var(--bg-raised, #161A1B);
  border: 1px solid var(--border, #323738);
  border-top-color: var(--border-lit, #4A4C4D);
  border-bottom-color: var(--ink, #010205);
  border-radius: var(--r-md, 5px);
}
.consent-panel::backdrop {
  background: color-mix(in srgb, var(--bg, #0A0D0E) var(--alpha-pane, 82%), transparent);
}

/* Answers a click, so it is allowed to move: DESIGN.md, Motion. */
.consent-panel[open],
.consent-panel[open]::backdrop {
  animation: consent-in var(--dur-base, 0.42s) var(--ease-enter, ease-out);
}
@keyframes consent-in { from { opacity: 0; } }

.consent-panel__form {
  display: flex;
  flex-direction: column;
  max-height: inherit;
  margin: 0;
}

.consent-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4, 16px);
  padding: var(--s-5, 20px) var(--s-6, 24px);
  border-bottom: 1px solid var(--border, #323738);
}
.consent-panel h2 {
  margin: 0;
  font-size: var(--fs-1, 20px);
  line-height: 1.2;
  color: var(--white, #E8F1F2);
}

/* An X drawn from two hairlines rather than a glyph, 44px to hit. */
.consent-panel__close {
  position: relative;
  flex: none;
  width: 44px;
  height: 44px;
  margin: calc(-1 * var(--s-3, 12px));
  padding: 0;
  border: 0;
  border-radius: var(--r-sm, 3px);
  background: none;
  cursor: pointer;
}
.consent-panel__close::before,
.consent-panel__close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 1px;
  background: var(--steel-300, #9AAFB8);
  transition: background-color var(--dur-quick, 0.18s) var(--ease-hover, ease);
}
.consent-panel__close::before { transform: translate(-50%, -50%) rotate(45deg); }
.consent-panel__close::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.consent-panel__close:hover::before,
.consent-panel__close:hover::after { background: var(--white, #E8F1F2); }

.consent-panel__scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-5, 20px) var(--s-6, 24px);
}
.consent-panel__scroll > p { margin: 0 0 var(--s-3, 12px); font-size: 15px; line-height: 1.6; }
.consent-panel__scroll strong { color: var(--steel-050, #D4E5EA); font-weight: 600; }

.consent-panel__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-1, 4px) var(--s-5, 20px);
}

.consent-panel h3 {
  margin: var(--s-6, 24px) 0 var(--s-3, 12px);
  font-size: var(--fs-0, 16px);
  color: var(--steel-050, #D4E5EA);
}

/* ---------- a category row ---------- */

.consent-cats { border-top: 1px solid var(--border, #323738); }

.consent-cat {
  position: relative;
  border-bottom: 1px solid var(--border, #323738);
}

.consent-cat__head {
  display: flex;
  align-items: center;
  gap: var(--s-3, 12px);
  min-height: 52px;
  padding-right: 120px;    /* room for the control beside it */
  list-style: none;
  cursor: pointer;
}
.consent-cat__head::-webkit-details-marker { display: none; }

/* Epsilon's + that turns to -, drawn as two hairlines; the upright one
   folds flat when the row opens. */
.consent-cat__head::before {
  content: "";
  flex: none;
  width: 12px;
  height: 12px;
  background:
    linear-gradient(var(--steel-300, #9AAFB8), var(--steel-300, #9AAFB8)) center / 12px 1px no-repeat,
    linear-gradient(var(--steel-300, #9AAFB8), var(--steel-300, #9AAFB8)) center / 1px 12px no-repeat;
  transition: background-size var(--dur-quick, 0.18s) var(--ease-hover, ease);
}
.consent-cat details[open] .consent-cat__head::before {
  background-size: 12px 1px, 1px 0;
}

.consent-cat__name {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--steel-050, #D4E5EA);
}

.consent-cat__body { padding: 0 0 var(--s-4, 16px) var(--s-6, 24px); }
.consent-cat__body p { margin: 0 0 var(--s-2, 8px); font-size: 14px; line-height: 1.6; color: var(--steel-500, #83979F); }
.consent-cat__body .consent-cat__note { color: var(--steel-100, #B4CAD2); }

.consent-cat__control {
  position: absolute;
  top: 0;
  right: 0;
  height: 52px;
  display: flex;
  align-items: center;
}

.consent-cat__always {
  font-size: var(--fs--1, 12.8px);
  font-weight: 600;
  color: var(--steel-100, #B4CAD2);
}

/* The switch: a native checkbox with role="switch", restyled. Steel,
   not amber, for the same reason as the buttons. */
.consent-cat__switch {
  appearance: none;
  position: relative;
  width: 40px;
  height: 22px;
  margin: 0;
  border: 1px solid var(--border-lit, #4A4C4D);
  border-radius: 11px;
  background: var(--charcoal, #202222);
  cursor: pointer;
  transition:
    background-color var(--dur-quick, 0.18s) var(--ease-hover, ease),
    border-color var(--dur-quick, 0.18s) var(--ease-hover, ease);
}
.consent-cat__switch::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--steel-700, #6B7C86);
  transition:
    transform var(--dur-quick, 0.18s) var(--ease-enter, ease-out),
    background-color var(--dur-quick, 0.18s) var(--ease-hover, ease);
}
.consent-cat__switch:checked {
  background: color-mix(in srgb, var(--steel-300, #9AAFB8) var(--alpha-mid, 34%), var(--charcoal, #202222));
  border-color: var(--steel-500, #83979F);
}
.consent-cat__switch:checked::after {
  transform: translateX(18px);
  background: var(--steel-050, #D4E5EA);
}
.consent-cat__switch:disabled { opacity: 0.5; cursor: not-allowed; }

.consent-panel__foot {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--s-3, 12px);
  padding: var(--s-4, 16px) var(--s-6, 24px);
  border-top: 1px solid var(--border, #323738);
}
@media (max-width: 560px) {
  .consent-panel__foot > * { flex: 1 1 0; }
}

@media (prefers-reduced-motion: reduce) {
  .consent-panel[open],
  .consent-panel[open]::backdrop { animation: none; }
  .consent__btn,
  .consent-cat__switch,
  .consent-cat__switch::after,
  .consent-cat__head::before { transition: none; }
}
